From f23855fb6854e46a5fa4e6cf0be2bcdab4f67547 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Fri, 16 Sep 2022 18:07:57 +0200 Subject: [PATCH] Included local plyer --- sbapp/plyer/__init__.py | 124 ++ sbapp/plyer/facades/__init__.py | 51 + sbapp/plyer/facades/accelerometer.py | 75 + sbapp/plyer/facades/audio.py | 103 + sbapp/plyer/facades/barometer.py | 36 + sbapp/plyer/facades/battery.py | 54 + sbapp/plyer/facades/bluetooth.py | 43 + sbapp/plyer/facades/brightness.py | 58 + sbapp/plyer/facades/call.py | 59 + sbapp/plyer/facades/camera.py | 88 + sbapp/plyer/facades/compass.py | 113 + sbapp/plyer/facades/cpu.py | 92 + sbapp/plyer/facades/devicename.py | 44 + sbapp/plyer/facades/email.py | 58 + sbapp/plyer/facades/filechooser.py | 74 + sbapp/plyer/facades/flash.py | 77 + sbapp/plyer/facades/gps.py | 96 + sbapp/plyer/facades/gravity.py | 38 + sbapp/plyer/facades/gyroscope.py | 126 ++ sbapp/plyer/facades/humidity.py | 34 + sbapp/plyer/facades/irblaster.py | 104 + sbapp/plyer/facades/keystore.py | 32 + sbapp/plyer/facades/light.py | 39 + sbapp/plyer/facades/notification.py | 93 + sbapp/plyer/facades/orientation.py | 81 + sbapp/plyer/facades/processors.py | 42 + sbapp/plyer/facades/proximity.py | 44 + sbapp/plyer/facades/screenshot.py | 57 + sbapp/plyer/facades/sms.py | 51 + sbapp/plyer/facades/spatialorientation.py | 54 + sbapp/plyer/facades/storagepath.py | 135 ++ sbapp/plyer/facades/stt.py | 188 ++ sbapp/plyer/facades/temperature.py | 41 + sbapp/plyer/facades/tts.py | 39 + sbapp/plyer/facades/uniqueid.py | 54 + sbapp/plyer/facades/vibrator.py | 97 + sbapp/plyer/facades/wifi.py | 187 ++ sbapp/plyer/platforms/__init__.py | 0 sbapp/plyer/platforms/android/__init__.py | 18 + .../plyer/platforms/android/accelerometer.py | 79 + sbapp/plyer/platforms/android/audio.py | 58 + sbapp/plyer/platforms/android/barometer.py | 65 + sbapp/plyer/platforms/android/battery.py | 47 + sbapp/plyer/platforms/android/bluetooth.py | 32 + sbapp/plyer/platforms/android/brightness.py | 34 + sbapp/plyer/platforms/android/call.py | 29 + sbapp/plyer/platforms/android/camera.py | 59 + sbapp/plyer/platforms/android/compass.py | 119 + sbapp/plyer/platforms/android/devicename.py | 33 + sbapp/plyer/platforms/android/email.py | 58 + sbapp/plyer/platforms/android/filechooser.py | 447 ++++ sbapp/plyer/platforms/android/flash.py | 55 + sbapp/plyer/platforms/android/gps.py | 82 + sbapp/plyer/platforms/android/gravity.py | 84 + sbapp/plyer/platforms/android/gyroscope.py | 119 + sbapp/plyer/platforms/android/humidity.py | 107 + sbapp/plyer/platforms/android/irblaster.py | 55 + sbapp/plyer/platforms/android/keystore.py | 25 + sbapp/plyer/platforms/android/light.py | 63 + sbapp/plyer/platforms/android/notification.py | 208 ++ sbapp/plyer/platforms/android/orientation.py | 43 + sbapp/plyer/platforms/android/proximity.py | 69 + sbapp/plyer/platforms/android/sms.py | 25 + .../platforms/android/spatialorientation.py | 118 + sbapp/plyer/platforms/android/storagepath.py | 69 + sbapp/plyer/platforms/android/stt.py | 252 +++ sbapp/plyer/platforms/android/temperature.py | 66 + sbapp/plyer/platforms/android/tts.py | 34 + sbapp/plyer/platforms/android/uniqueid.py | 28 + sbapp/plyer/platforms/android/vibrator.py | 63 + sbapp/plyer/platforms/ios/__init__.py | 0 sbapp/plyer/platforms/ios/accelerometer.py | 34 + sbapp/plyer/platforms/ios/barometer.py | 31 + sbapp/plyer/platforms/ios/battery.py | 47 + sbapp/plyer/platforms/ios/brightness.py | 27 + sbapp/plyer/platforms/ios/call.py | 29 + sbapp/plyer/platforms/ios/camera.py | 52 + sbapp/plyer/platforms/ios/compass.py | 43 + sbapp/plyer/platforms/ios/email.py | 52 + sbapp/plyer/platforms/ios/filechooser.py | 81 + sbapp/plyer/platforms/ios/flash.py | 50 + sbapp/plyer/platforms/ios/gps.py | 80 + sbapp/plyer/platforms/ios/gravity.py | 31 + sbapp/plyer/platforms/ios/gyroscope.py | 55 + sbapp/plyer/platforms/ios/keystore.py | 23 + sbapp/plyer/platforms/ios/sms.py | 43 + .../plyer/platforms/ios/spatialorientation.py | 31 + sbapp/plyer/platforms/ios/storagepath.py | 62 + sbapp/plyer/platforms/ios/tts.py | 37 + sbapp/plyer/platforms/ios/uniqueid.py | 27 + sbapp/plyer/platforms/ios/vibrator.py | 43 + sbapp/plyer/platforms/linux/__init__.py | 0 sbapp/plyer/platforms/linux/accelerometer.py | 35 + sbapp/plyer/platforms/linux/battery.py | 102 + sbapp/plyer/platforms/linux/brightness.py | 29 + sbapp/plyer/platforms/linux/cpu.py | 116 + sbapp/plyer/platforms/linux/devicename.py | 23 + sbapp/plyer/platforms/linux/email.py | 47 + sbapp/plyer/platforms/linux/filechooser.py | 269 +++ sbapp/plyer/platforms/linux/keystore.py | 19 + sbapp/plyer/platforms/linux/notification.py | 108 + sbapp/plyer/platforms/linux/orientation.py | 27 + sbapp/plyer/platforms/linux/processors.py | 37 + sbapp/plyer/platforms/linux/screenshot.py | 29 + sbapp/plyer/platforms/linux/storagepath.py | 72 + sbapp/plyer/platforms/linux/tts.py | 25 + sbapp/plyer/platforms/linux/uniqueid.py | 47 + sbapp/plyer/platforms/linux/wifi.py | 482 ++++ sbapp/plyer/platforms/macosx/__init__.py | 0 sbapp/plyer/platforms/macosx/accelerometer.py | 25 + sbapp/plyer/platforms/macosx/audio.py | 90 + sbapp/plyer/platforms/macosx/battery.py | 59 + sbapp/plyer/platforms/macosx/bluetooth.py | 54 + sbapp/plyer/platforms/macosx/cpu.py | 62 + sbapp/plyer/platforms/macosx/devicename.py | 23 + sbapp/plyer/platforms/macosx/email.py | 49 + sbapp/plyer/platforms/macosx/filechooser.py | 126 ++ sbapp/plyer/platforms/macosx/keystore.py | 19 + sbapp/plyer/platforms/macosx/libs/__init__.py | 0 .../macosx/libs/osx_motion_sensor.py | 128 ++ .../plyer/platforms/macosx/libs/osx_paths.py | 21 + sbapp/plyer/platforms/macosx/notification.py | 51 + sbapp/plyer/platforms/macosx/screenshot.py | 27 + sbapp/plyer/platforms/macosx/storagepath.py | 62 + sbapp/plyer/platforms/macosx/tts.py | 25 + sbapp/plyer/platforms/macosx/uniqueid.py | 47 + sbapp/plyer/platforms/macosx/wifi.py | 147 ++ sbapp/plyer/platforms/win/__init__.py | 0 sbapp/plyer/platforms/win/audio.py | 398 ++++ sbapp/plyer/platforms/win/battery.py | 36 + sbapp/plyer/platforms/win/cpu.py | 252 +++ sbapp/plyer/platforms/win/devicename.py | 23 + sbapp/plyer/platforms/win/email.py | 46 + sbapp/plyer/platforms/win/filechooser.py | 162 ++ sbapp/plyer/platforms/win/keystore.py | 19 + sbapp/plyer/platforms/win/libs/__init__.py | 0 sbapp/plyer/platforms/win/libs/balloontip.py | 206 ++ .../plyer/platforms/win/libs/batterystatus.py | 24 + sbapp/plyer/platforms/win/libs/wifi_defs.py | 572 +++++ .../plyer/platforms/win/libs/win_api_defs.py | 250 +++ sbapp/plyer/platforms/win/notification.py | 24 + sbapp/plyer/platforms/win/screenshot.py | 107 + sbapp/plyer/platforms/win/storagepath.py | 53 + sbapp/plyer/platforms/win/tts.py | 16 + sbapp/plyer/platforms/win/uniqueid.py | 36 + sbapp/plyer/platforms/win/wifi.py | 121 + sbapp/plyer/tests/__init__.py | 0 sbapp/plyer/tests/common.py | 76 + sbapp/plyer/tests/images/kivy32.ico | Bin 0 -> 4286 bytes sbapp/plyer/tests/test_audio.py | 110 + sbapp/plyer/tests/test_battery.py | 413 ++++ sbapp/plyer/tests/test_bluetooth.py | 144 ++ sbapp/plyer/tests/test_cpu.py | 262 +++ sbapp/plyer/tests/test_devicename.py | 77 + sbapp/plyer/tests/test_email.py | 48 + sbapp/plyer/tests/test_facade.py | 183 ++ sbapp/plyer/tests/test_notification.py | 209 ++ sbapp/plyer/tests/test_screenshot.py | 137 ++ sbapp/plyer/tests/test_storagepath.py | 72 + sbapp/plyer/tests/test_uniqueid.py | 78 + sbapp/plyer/tests/test_utils.py | 417 ++++ sbapp/plyer/tools/pep8checker/pep8.py | 1953 +++++++++++++++++ sbapp/plyer/tools/pep8checker/pep8base.html | 70 + sbapp/plyer/tools/pep8checker/pep8kivy.py | 116 + .../tools/pep8checker/pre-commit.githook | 78 + sbapp/plyer/utils.py | 290 +++ 166 files changed, 15862 insertions(+) create mode 100644 sbapp/plyer/__init__.py create mode 100644 sbapp/plyer/facades/__init__.py create mode 100644 sbapp/plyer/facades/accelerometer.py create mode 100644 sbapp/plyer/facades/audio.py create mode 100644 sbapp/plyer/facades/barometer.py create mode 100644 sbapp/plyer/facades/battery.py create mode 100644 sbapp/plyer/facades/bluetooth.py create mode 100755 sbapp/plyer/facades/brightness.py create mode 100644 sbapp/plyer/facades/call.py create mode 100644 sbapp/plyer/facades/camera.py create mode 100644 sbapp/plyer/facades/compass.py create mode 100644 sbapp/plyer/facades/cpu.py create mode 100644 sbapp/plyer/facades/devicename.py create mode 100644 sbapp/plyer/facades/email.py create mode 100644 sbapp/plyer/facades/filechooser.py create mode 100644 sbapp/plyer/facades/flash.py create mode 100644 sbapp/plyer/facades/gps.py create mode 100644 sbapp/plyer/facades/gravity.py create mode 100644 sbapp/plyer/facades/gyroscope.py create mode 100644 sbapp/plyer/facades/humidity.py create mode 100644 sbapp/plyer/facades/irblaster.py create mode 100644 sbapp/plyer/facades/keystore.py create mode 100644 sbapp/plyer/facades/light.py create mode 100644 sbapp/plyer/facades/notification.py create mode 100644 sbapp/plyer/facades/orientation.py create mode 100644 sbapp/plyer/facades/processors.py create mode 100644 sbapp/plyer/facades/proximity.py create mode 100644 sbapp/plyer/facades/screenshot.py create mode 100644 sbapp/plyer/facades/sms.py create mode 100644 sbapp/plyer/facades/spatialorientation.py create mode 100644 sbapp/plyer/facades/storagepath.py create mode 100644 sbapp/plyer/facades/stt.py create mode 100644 sbapp/plyer/facades/temperature.py create mode 100644 sbapp/plyer/facades/tts.py create mode 100644 sbapp/plyer/facades/uniqueid.py create mode 100644 sbapp/plyer/facades/vibrator.py create mode 100644 sbapp/plyer/facades/wifi.py create mode 100644 sbapp/plyer/platforms/__init__.py create mode 100644 sbapp/plyer/platforms/android/__init__.py create mode 100644 sbapp/plyer/platforms/android/accelerometer.py create mode 100644 sbapp/plyer/platforms/android/audio.py create mode 100644 sbapp/plyer/platforms/android/barometer.py create mode 100644 sbapp/plyer/platforms/android/battery.py create mode 100644 sbapp/plyer/platforms/android/bluetooth.py create mode 100755 sbapp/plyer/platforms/android/brightness.py create mode 100644 sbapp/plyer/platforms/android/call.py create mode 100644 sbapp/plyer/platforms/android/camera.py create mode 100644 sbapp/plyer/platforms/android/compass.py create mode 100644 sbapp/plyer/platforms/android/devicename.py create mode 100644 sbapp/plyer/platforms/android/email.py create mode 100644 sbapp/plyer/platforms/android/filechooser.py create mode 100644 sbapp/plyer/platforms/android/flash.py create mode 100644 sbapp/plyer/platforms/android/gps.py create mode 100644 sbapp/plyer/platforms/android/gravity.py create mode 100644 sbapp/plyer/platforms/android/gyroscope.py create mode 100644 sbapp/plyer/platforms/android/humidity.py create mode 100644 sbapp/plyer/platforms/android/irblaster.py create mode 100644 sbapp/plyer/platforms/android/keystore.py create mode 100644 sbapp/plyer/platforms/android/light.py create mode 100644 sbapp/plyer/platforms/android/notification.py create mode 100644 sbapp/plyer/platforms/android/orientation.py create mode 100644 sbapp/plyer/platforms/android/proximity.py create mode 100644 sbapp/plyer/platforms/android/sms.py create mode 100644 sbapp/plyer/platforms/android/spatialorientation.py create mode 100755 sbapp/plyer/platforms/android/storagepath.py create mode 100644 sbapp/plyer/platforms/android/stt.py create mode 100644 sbapp/plyer/platforms/android/temperature.py create mode 100644 sbapp/plyer/platforms/android/tts.py create mode 100644 sbapp/plyer/platforms/android/uniqueid.py create mode 100644 sbapp/plyer/platforms/android/vibrator.py create mode 100644 sbapp/plyer/platforms/ios/__init__.py create mode 100644 sbapp/plyer/platforms/ios/accelerometer.py create mode 100644 sbapp/plyer/platforms/ios/barometer.py create mode 100644 sbapp/plyer/platforms/ios/battery.py create mode 100644 sbapp/plyer/platforms/ios/brightness.py create mode 100644 sbapp/plyer/platforms/ios/call.py create mode 100644 sbapp/plyer/platforms/ios/camera.py create mode 100644 sbapp/plyer/platforms/ios/compass.py create mode 100644 sbapp/plyer/platforms/ios/email.py create mode 100644 sbapp/plyer/platforms/ios/filechooser.py create mode 100644 sbapp/plyer/platforms/ios/flash.py create mode 100644 sbapp/plyer/platforms/ios/gps.py create mode 100644 sbapp/plyer/platforms/ios/gravity.py create mode 100644 sbapp/plyer/platforms/ios/gyroscope.py create mode 100644 sbapp/plyer/platforms/ios/keystore.py create mode 100644 sbapp/plyer/platforms/ios/sms.py create mode 100644 sbapp/plyer/platforms/ios/spatialorientation.py create mode 100644 sbapp/plyer/platforms/ios/storagepath.py create mode 100644 sbapp/plyer/platforms/ios/tts.py create mode 100644 sbapp/plyer/platforms/ios/uniqueid.py create mode 100644 sbapp/plyer/platforms/ios/vibrator.py create mode 100644 sbapp/plyer/platforms/linux/__init__.py create mode 100644 sbapp/plyer/platforms/linux/accelerometer.py create mode 100644 sbapp/plyer/platforms/linux/battery.py create mode 100755 sbapp/plyer/platforms/linux/brightness.py create mode 100644 sbapp/plyer/platforms/linux/cpu.py create mode 100644 sbapp/plyer/platforms/linux/devicename.py create mode 100644 sbapp/plyer/platforms/linux/email.py create mode 100644 sbapp/plyer/platforms/linux/filechooser.py create mode 100644 sbapp/plyer/platforms/linux/keystore.py create mode 100644 sbapp/plyer/platforms/linux/notification.py create mode 100644 sbapp/plyer/platforms/linux/orientation.py create mode 100644 sbapp/plyer/platforms/linux/processors.py create mode 100644 sbapp/plyer/platforms/linux/screenshot.py create mode 100755 sbapp/plyer/platforms/linux/storagepath.py create mode 100644 sbapp/plyer/platforms/linux/tts.py create mode 100644 sbapp/plyer/platforms/linux/uniqueid.py create mode 100644 sbapp/plyer/platforms/linux/wifi.py create mode 100644 sbapp/plyer/platforms/macosx/__init__.py create mode 100644 sbapp/plyer/platforms/macosx/accelerometer.py create mode 100644 sbapp/plyer/platforms/macosx/audio.py create mode 100644 sbapp/plyer/platforms/macosx/battery.py create mode 100644 sbapp/plyer/platforms/macosx/bluetooth.py create mode 100644 sbapp/plyer/platforms/macosx/cpu.py create mode 100644 sbapp/plyer/platforms/macosx/devicename.py create mode 100644 sbapp/plyer/platforms/macosx/email.py create mode 100644 sbapp/plyer/platforms/macosx/filechooser.py create mode 100644 sbapp/plyer/platforms/macosx/keystore.py create mode 100644 sbapp/plyer/platforms/macosx/libs/__init__.py create mode 100644 sbapp/plyer/platforms/macosx/libs/osx_motion_sensor.py create mode 100644 sbapp/plyer/platforms/macosx/libs/osx_paths.py create mode 100644 sbapp/plyer/platforms/macosx/notification.py create mode 100644 sbapp/plyer/platforms/macosx/screenshot.py create mode 100644 sbapp/plyer/platforms/macosx/storagepath.py create mode 100644 sbapp/plyer/platforms/macosx/tts.py create mode 100644 sbapp/plyer/platforms/macosx/uniqueid.py create mode 100644 sbapp/plyer/platforms/macosx/wifi.py create mode 100644 sbapp/plyer/platforms/win/__init__.py create mode 100644 sbapp/plyer/platforms/win/audio.py create mode 100644 sbapp/plyer/platforms/win/battery.py create mode 100644 sbapp/plyer/platforms/win/cpu.py create mode 100644 sbapp/plyer/platforms/win/devicename.py create mode 100644 sbapp/plyer/platforms/win/email.py create mode 100644 sbapp/plyer/platforms/win/filechooser.py create mode 100644 sbapp/plyer/platforms/win/keystore.py create mode 100644 sbapp/plyer/platforms/win/libs/__init__.py create mode 100644 sbapp/plyer/platforms/win/libs/balloontip.py create mode 100644 sbapp/plyer/platforms/win/libs/batterystatus.py create mode 100644 sbapp/plyer/platforms/win/libs/wifi_defs.py create mode 100755 sbapp/plyer/platforms/win/libs/win_api_defs.py create mode 100644 sbapp/plyer/platforms/win/notification.py create mode 100644 sbapp/plyer/platforms/win/screenshot.py create mode 100755 sbapp/plyer/platforms/win/storagepath.py create mode 100644 sbapp/plyer/platforms/win/tts.py create mode 100644 sbapp/plyer/platforms/win/uniqueid.py create mode 100644 sbapp/plyer/platforms/win/wifi.py create mode 100644 sbapp/plyer/tests/__init__.py create mode 100644 sbapp/plyer/tests/common.py create mode 100644 sbapp/plyer/tests/images/kivy32.ico create mode 100644 sbapp/plyer/tests/test_audio.py create mode 100644 sbapp/plyer/tests/test_battery.py create mode 100644 sbapp/plyer/tests/test_bluetooth.py create mode 100644 sbapp/plyer/tests/test_cpu.py create mode 100644 sbapp/plyer/tests/test_devicename.py create mode 100644 sbapp/plyer/tests/test_email.py create mode 100644 sbapp/plyer/tests/test_facade.py create mode 100644 sbapp/plyer/tests/test_notification.py create mode 100644 sbapp/plyer/tests/test_screenshot.py create mode 100644 sbapp/plyer/tests/test_storagepath.py create mode 100644 sbapp/plyer/tests/test_uniqueid.py create mode 100644 sbapp/plyer/tests/test_utils.py create mode 100644 sbapp/plyer/tools/pep8checker/pep8.py create mode 100644 sbapp/plyer/tools/pep8checker/pep8base.html create mode 100644 sbapp/plyer/tools/pep8checker/pep8kivy.py create mode 100755 sbapp/plyer/tools/pep8checker/pre-commit.githook create mode 100644 sbapp/plyer/utils.py diff --git a/sbapp/plyer/__init__.py b/sbapp/plyer/__init__.py new file mode 100644 index 0000000..ab87901 --- /dev/null +++ b/sbapp/plyer/__init__.py @@ -0,0 +1,124 @@ +''' +Plyer +===== + +''' + +__all__ = ( + 'accelerometer', 'audio', 'barometer', 'battery', 'bluetooth', + 'brightness', 'call', 'camera', 'compass', 'cpu', 'email', 'filechooser', + 'flash', 'gps', 'gravity', 'gyroscope', 'humidity', 'irblaster', + 'keystore', 'light', 'notification', 'orientation', 'processors', + 'proximity', 'screenshot', 'sms', 'spatialorientation', 'storagepath', + 'stt', 'temperature', 'tts', 'uniqueid', 'vibrator', 'wifi', 'devicename' +) + +__version__ = '2.1.0.dev0' + + +from plyer import facades +from plyer.utils import Proxy + +#: Accelerometer proxy to :class:`plyer.facades.Accelerometer` +accelerometer = Proxy('accelerometer', facades.Accelerometer) + +#: Keyring proxy to :class::`plyer.facades.Keystore` +keystore = Proxy('keystore', facades.Keystore) + +#: Audio proxy to :class:`plyer.facades.Audio` +audio = Proxy('audio', facades.Audio) + +#: Barometer proxy to :class:`plyer.facades.Barometer` +barometer = Proxy('barometer', facades.Barometer) + +#: Battery proxy to :class:`plyer.facades.Battery` +battery = Proxy('battery', facades.Battery) + +#: Call proxy to :class `plyer.facades.Call` +call = Proxy('call', facades.Call) + +#: Compass proxy to :class:`plyer.facades.Compass` +compass = Proxy('compass', facades.Compass) + +#: Camera proxy to :class:`plyer.facades.Camera` +camera = Proxy('camera', facades.Camera) + +#: Email proxy to :class:`plyer.facades.Email` +email = Proxy('email', facades.Email) + +#: FileChooser proxy to :class:`plyer.facades.FileChooser` +filechooser = Proxy('filechooser', facades.FileChooser) + +#: GPS proxy to :class:`plyer.facades.GPS` +gps = Proxy('gps', facades.GPS) + +#: Gravity proxy to :class:`plyer.facades.Gravity` +gravity = Proxy('gravity', facades.Gravity) + +#: Gyroscope proxy to :class:`plyer.facades.Gyroscope` +gyroscope = Proxy('gyroscope', facades.Gyroscope) + +#: IrBlaster proxy to :class:`plyer.facades.IrBlaster` +irblaster = Proxy('irblaster', facades.IrBlaster) + +#: Light proxy to :class:`plyer.facades.Light` +light = Proxy('light', facades.Light) + +#: Orientation proxy to :class:`plyer.facades.Orientation` +orientation = Proxy('orientation', facades.Orientation) + +#: Notification proxy to :class:`plyer.facades.Notification` +notification = Proxy('notification', facades.Notification) + +#: Proximity proxy to :class:`plyer.facades.Proximity` +proximity = Proxy('proximity', facades.Proximity) + +#: Sms proxy to :class:`plyer.facades.Sms` +sms = Proxy('sms', facades.Sms) + +#: Speech proxy to :class:`plyer.facades.STT` +stt = Proxy('stt', facades.STT) + +#: TTS proxy to :class:`plyer.facades.TTS` +tts = Proxy('tts', facades.TTS) + +#: UniqueID proxy to :class:`plyer.facades.UniqueID` +uniqueid = Proxy('uniqueid', facades.UniqueID) + +#: Vibrator proxy to :class:`plyer.facades.Vibrator` +vibrator = Proxy('vibrator', facades.Vibrator) + +#: Flash proxy to :class:`plyer.facades.Flash` +flash = Proxy('flash', facades.Flash) + +#: Wifi proxy to :class:`plyer.facades.Wifi` +wifi = Proxy('wifi', facades.Wifi) + +#: Temperature proxy to :class:`plyer.facades.Temperature` +temperature = Proxy('temperature', facades.Temperature) + +#: Humidity proxy to :class:`plyer.facades.Humidity` +humidity = Proxy('humidity', facades.Humidity) +#: SpatialOrientation proxy to :class:`plyer.facades.SpatialOrientation` +spatialorientation = Proxy('spatialorientation', facades.SpatialOrientation) + +#: Brightness proxy to :class:`plyer.facades.Brightness` +brightness = Proxy('brightness', facades.Brightness) + +#: StoragePath proxy to :class:`plyer.facades.StoragePath` +storagepath = Proxy('storagepath', facades.StoragePath) + +#: Bluetooth proxy to :class:`plyer.facades.Bluetooth` +bluetooth = Proxy('bluetooth', facades.Bluetooth) + +#: Processors proxy to :class:`plyer.facades.Processors` +processors = Proxy('processors', facades.Processors) + +#: Processors proxy to :class:`plyer.facades.CPU` +cpu = Proxy('cpu', facades.CPU) + +#: Screenshot proxy to :class:`plyer.facades.Screenshot` +screenshot = Proxy('screenshot', facades.Screenshot) + +#: devicename proxy to :class:`plyer.facades.DeviceName` +devicename = Proxy('devicename', facades.DeviceName) diff --git a/sbapp/plyer/facades/__init__.py b/sbapp/plyer/facades/__init__.py new file mode 100644 index 0000000..c1e2560 --- /dev/null +++ b/sbapp/plyer/facades/__init__.py @@ -0,0 +1,51 @@ +''' +Facades +======= + +Interface of all the features available. + +''' + +__all__ = ('Accelerometer', 'Audio', 'Barometer', 'Battery', 'Call', 'Camera', + 'Compass', 'Email', 'FileChooser', 'GPS', 'Gravity', 'Gyroscope', + 'IrBlaster', 'Light', 'Orientation', 'Notification', 'Proximity', + 'Sms', 'TTS', 'UniqueID', 'Vibrator', 'Wifi', 'Flash', 'CPU', + 'Temperature', 'Humidity', 'SpatialOrientation', 'Brightness', + 'Processors', 'StoragePath', 'Keystore', 'Bluetooth', 'Screenshot', + 'STT', 'DeviceName') + +from plyer.facades.accelerometer import Accelerometer +from plyer.facades.audio import Audio +from plyer.facades.barometer import Barometer +from plyer.facades.battery import Battery +from plyer.facades.call import Call +from plyer.facades.camera import Camera +from plyer.facades.compass import Compass +from plyer.facades.email import Email +from plyer.facades.filechooser import FileChooser +from plyer.facades.flash import Flash +from plyer.facades.gps import GPS +from plyer.facades.gravity import Gravity +from plyer.facades.gyroscope import Gyroscope +from plyer.facades.irblaster import IrBlaster +from plyer.facades.light import Light +from plyer.facades.proximity import Proximity +from plyer.facades.orientation import Orientation +from plyer.facades.notification import Notification +from plyer.facades.sms import Sms +from plyer.facades.stt import STT +from plyer.facades.tts import TTS +from plyer.facades.uniqueid import UniqueID +from plyer.facades.vibrator import Vibrator +from plyer.facades.wifi import Wifi +from plyer.facades.temperature import Temperature +from plyer.facades.humidity import Humidity +from plyer.facades.spatialorientation import SpatialOrientation +from plyer.facades.brightness import Brightness +from plyer.facades.keystore import Keystore +from plyer.facades.storagepath import StoragePath +from plyer.facades.bluetooth import Bluetooth +from plyer.facades.processors import Processors +from plyer.facades.cpu import CPU +from plyer.facades.screenshot import Screenshot +from plyer.facades.devicename import DeviceName diff --git a/sbapp/plyer/facades/accelerometer.py b/sbapp/plyer/facades/accelerometer.py new file mode 100644 index 0000000..d8a7143 --- /dev/null +++ b/sbapp/plyer/facades/accelerometer.py @@ -0,0 +1,75 @@ +''' +Accelerometer +============ + +The accelerometer is a motion sensor that detects the change (delta) in +movement relative to the current device orientation, in three dimensions +along the x, y, and z axis. + +The :class:`Accelerometer` provides access to public methods to +use accelerometer of your device. + +Simple Examples +--------------- + +To enable accelerometer:: + + >>> from plyer import accelerometer + >>> accelerometer.enable() + +To disable accelerometer:: + + >>> accelerometer.disable() + +To get the acceleration:: + + >>> accelerometer.acceleration + (-10.048464775085449, 6.825869083404541, 7.7260890007019043) + +Supported Plaforms +------------------ +Android, iOS, OS X, Linux + +''' + + +class Accelerometer: + ''' + Accelerometer facade. + ''' + + @property + def acceleration(self): + ''' + Property that returns values of the current acceleration + sensors, as a (x, y, z) tuple. Returns (None, None, None) + if no data is currently available. + ''' + return self.get_acceleration() + + def enable(self): + ''' + Activate the accelerometer sensor. Throws an error if the + hardware is not available or not implemented on. + ''' + self._enable() + + def disable(self): + ''' + Disable the accelerometer sensor. + ''' + self._disable() + + def get_acceleration(self): + return self._get_acceleration() + + # private + + def _enable(self): + raise NotImplementedError() + + def _disable(self): + raise NotImplementedError() + + def _get_acceleration(self): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/audio.py b/sbapp/plyer/facades/audio.py new file mode 100644 index 0000000..c5e1db5 --- /dev/null +++ b/sbapp/plyer/facades/audio.py @@ -0,0 +1,103 @@ +''' +Audio +===== + +The :class:`Audio` is used for recording audio. + +Default path for recording is set in platform implementation. + +.. note:: + On Android the `RECORD_AUDIO`, `WAKE_LOCK` permissions are needed. + +Simple Examples +--------------- + +To get the file path:: + + >>> audio.file_path + '/sdcard/testrecorder.3gp' + +To set the file path:: + + >>> import os + >>> current_list = os.listdir('.') + ['/sdcard/testrecorder.3gp', '/sdcard/testrecorder1.3gp', + '/sdcard/testrecorder2.3gp', '/sdcard/testrecorder3.3gp'] + >>> file_path = current_list[2] + >>> audio.file_path = file_path + +To start recording:: + + >>> from plyer import audio + >>> audio.start() + +To stop recording:: + + >>> audio.stop() + +To play recording:: + + >>> audio.play() + +Supported Platforms +------------------- +Android, Windows, macOS + +''' + + +class Audio: + ''' + Audio facade. + ''' + + state = 'ready' + _file_path = '' + + def __init__(self, file_path=None): + super().__init__() + self._file_path = file_path or self._file_path + + def start(self): + ''' + Start record. + ''' + self._start() + self.state = 'recording' + + def stop(self): + ''' + Stop record. + ''' + self._stop() + self.state = 'ready' + + def play(self): + ''' + Play current recording. + ''' + self._play() + self.state = 'playing' + + @property + def file_path(self): + return self._file_path + + @file_path.setter + def file_path(self, location): + ''' + Location of the recording. + ''' + assert isinstance(location, str), 'Location must be string or unicode' + self._file_path = location + + # private + + def _start(self): + raise NotImplementedError() + + def _stop(self): + raise NotImplementedError() + + def _play(self): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/barometer.py b/sbapp/plyer/facades/barometer.py new file mode 100644 index 0000000..ec97b8b --- /dev/null +++ b/sbapp/plyer/facades/barometer.py @@ -0,0 +1,36 @@ +class Barometer: + '''Barometer facade. + + Barometer sensor is used to measure the ambient air pressure in hPa. + + With method `enable` you can turn on pressure sensor and 'disable' + method stops the sensor. + + Use property `pressure` to get current air pressure in hPa. + + .. versionadded:: 1.2.5 + + Supported Platforms:: Android, iOS + ''' + + @property + def pressure(self): + '''Current air pressure in hPa.''' + return self._get_pressure() + + def _get_pressure(self, **kwargs): + raise NotImplementedError() + + def _enable(self, **kwargs): + raise NotImplementedError() + + def enable(self): + '''Enable barometer sensor.''' + self._enable() + + def _disable(self, **kwargs): + raise NotImplementedError() + + def disable(self): + '''Disable barometer sensor.''' + self._disable() diff --git a/sbapp/plyer/facades/battery.py b/sbapp/plyer/facades/battery.py new file mode 100644 index 0000000..f0ee147 --- /dev/null +++ b/sbapp/plyer/facades/battery.py @@ -0,0 +1,54 @@ +''' +Battery +======= + +The :class:`Battery` provides information about the battery of your device. + +.. note:: + On Android the `BATTERY_STATS` permission is needed. + +Simple Example +--------------- + +To get battery status:: + + >>> from plyer import battery + >>> battery.status + {'percentage': 82.0, 'isCharging': False} + +Supported Platforms +------------------- +Android, iOS, Windows, OS X, Linux + +''' + + +class Battery: + ''' + Battery info facade. + ''' + + @property + def status(self): + ''' + Property that contains a dict with the following fields: + * **isCharging** *(bool)*: Battery is charging + * **percentage** *(float)*: Battery charge remaining + + .. warning:: + If any of the fields is not readable, it is set as + None. + ''' + return self.get_state() + + def get_state(self): + ''' + Public method for filling battery.status via platform-specific + API in plyer.platforms. + ''' + return self._get_state() + + # private + + def _get_state(self): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/bluetooth.py b/sbapp/plyer/facades/bluetooth.py new file mode 100644 index 0000000..a76460d --- /dev/null +++ b/sbapp/plyer/facades/bluetooth.py @@ -0,0 +1,43 @@ +'''Bluetooth facade. + +Returns the following: + +* Bluetooth info + +Simple Example +-------------- + +To get the bluetooth status info:: +todo: will be extended to get additional bluetooth info +todo: will be extended to allow bluetooth connections etc. + + >>> from plyer import bluetooth + >>> bluetooth + 'on' or 'off' + +Supported Platforms +------------------- +Android, OS X + +''' + + +class Bluetooth: + ''' + Bluetooth facade. + ''' + + @property + def info(self): + ''' + Property that returns the info (currently status) of the bluetooth. + ''' + return self.get_info() + + def get_info(self): + return self._get_info() + + # private + + def _get_info(self, **kwargs): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/brightness.py b/sbapp/plyer/facades/brightness.py new file mode 100755 index 0000000..538724b --- /dev/null +++ b/sbapp/plyer/facades/brightness.py @@ -0,0 +1,58 @@ +''' +Brightness +========== + +This API helps you to control the brightness of your primary display screen. + +The :class:`Brightness` provides access to public methods to control the +brightness of screen. + +NOTE:: For Android, make sure to add permission, WRITE_SETTINGS + +Simple Examples +--------------- +To know the current brightness level of device:: + + >>> from plyer import brightness + >>> brightness.current_level() + +To set the brightness level to half of maximum:: + + >>> from plyer import brightness + >>> brightness.set_level(50) + +Supported Platforms +------------------- +Android, iOS, Linux +''' + + +class Brightness: + ''' + Brightness facade. + ''' + + def current_level(self): + ''' + Know the current level of device's brightness. + ''' + return self._current_level() + + def set_level(self, level): + ''' + Adjust the brightness of the screen. + Minimum brightness level:: 1 + Maximum brightness level:: 100 + + :param level: New level of brightness between 1 and 100 + :type level: int + ''' + return self._set_level(level) + + # private + + def _set_level(self, level): + raise NotImplementedError() + + def _current_level(self): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/call.py b/sbapp/plyer/facades/call.py new file mode 100644 index 0000000..f551b80 --- /dev/null +++ b/sbapp/plyer/facades/call.py @@ -0,0 +1,59 @@ +''' +Call +==== + +The :class:`Call` provides access to calling feature of your device. + +.. note:: + - On Android your app needs the `CALL_PHONE` or `CALL_PRIVILEGED` + permission in order to make calls. + + - Dialing call feature in not supported yet in iOS devices. + +Simple Examples +--------------- + +To make call:: + + >>> from plyer import call + >>> tel = 9999222299 + >>> call.makecall(tel=tel) + +To dial call:: + + >>> call.dialcall() + +Supported Platforms +------------------- +Android, iOS + +''' + + +class Call: + ''' + Call facade. + ''' + + def makecall(self, tel): + ''' + Make calls using your device. + + :param tel: The reciever + :type tel: number + ''' + self._makecall(tel=tel) + + def dialcall(self): + ''' + Opens dialing interface. + ''' + self._dialcall() + + # private + + def _makecall(self, **kwargs): + raise NotImplementedError() + + def _dialcall(self, **kwargs): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/camera.py b/sbapp/plyer/facades/camera.py new file mode 100644 index 0000000..ebb2615 --- /dev/null +++ b/sbapp/plyer/facades/camera.py @@ -0,0 +1,88 @@ +''' +Camera +====== + +The :class:`Camera` is to capture pictures and make videos. + +.. note:: + - On Android the `CAMERA` , `WRITE_EXTERNAL_STORAGE`, + `READ_EXTERNAL_STORAGE` permissions are needed. + +Simple Examples +--------------- + +Setup callback function. + + >>> from os.path import exists, join + >>> from plyer import camera + >>> def camera_callback(filepath): + >>> if(exists(filepath)): + >>> print "saved" + >>> else: + >>> print "unable to save." + >>> filepath = 'path/to/your/file' + >>> # e.g: filepath = join(App.get_running_app().user_data_dir, file_name) + +To take picture:: + + >>> file_name = "test.jpg" + >>> camera.take_picture(filename=file_name, + >>> on_complete=camera_callback) + +Ta take a video:: + + >>> file_name = "test.mp4" + >>> camera.take_video(filename=file_name, + >>> on_complete=camera_callback) + +Supported Platforms +------------------- +Android, iOS + +''' + + +class Camera: + ''' + Camera facade. + ''' + + def take_picture(self, filename, on_complete): + '''Ask the OS to capture a picture, and store it at filename. + + When the capture is done, on_complete will be called with the filename + as an argument. If the callback returns True, the filename will be + unlinked. + + :param filename: Name of the image file + :param on_complete: Callback that will be called when the operation is + done + + :type filename: str + :type on_complete: callable + ''' + self._take_picture(filename=filename, on_complete=on_complete) + + def take_video(self, filename, on_complete): + '''Ask the OS to capture a video, and store it at filename. + + When the capture is done, on_complete will be called with the filename + as an argument. If the callback returns True, the filename will be + unlinked. + + :param filename: Name of the video file + :param on_complete: Callback that will be called when the operation is + done + + :type filename: str + :type on_complete: callable + ''' + self._take_video(filename=filename, on_complete=on_complete) + + # private + + def _take_picture(self, **kwargs): + raise NotImplementedError() + + def _take_video(self, **kwargs): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/compass.py b/sbapp/plyer/facades/compass.py new file mode 100644 index 0000000..864c58b --- /dev/null +++ b/sbapp/plyer/facades/compass.py @@ -0,0 +1,113 @@ +''' +Compass +======= + +The :class:`Compass` provides access to public methods to use compass of your +device. + +Simple Examples +--------------- + +To enable compass:: + + >>> from plyer import compass + >>> compass.enable() + +To disable compass:: + + >>> compass.disable() + +To get the field:: + + >>> compass.field() + (-23.721826553344727, -5.7114701271057129, -36.749668121337891) + +To get the uncalibrated field along with iron bias estimation:: + + >>> compass.field_uncalib() + (a,b,c,x,y,z) + # a,b,c denote the Geomagnetic field strength + # (without hard iron calibration) along the three axes. + # x,y,z denote the Iron bias estimation along the three axes. + +Supported Platforms +------------------- +Android, iOS + +''' + + +class Compass: + '''Compass facade. + + .. versionadded:: 1.2.0 + ''' + + @property + def orientation(self): + ''' + WARNING:: This property is deprecated after API level 8. + Use `compass.field` instead. + + Property that returns values of the current compass + (magnetic field) sensors, as a (x, y, z) tuple. + Returns (None, None, None) if no data is currently available. + ''' + return self.get_orientation() + + @property + def field(self): + ''' + .. versionadded:: 1.3.1 + + Property that returns values of the current compass + (magnetic field) sensors, as a (x, y, z) tuple. + Returns (None, None, None) if no data is currently available. + ''' + return self.get_orientation() + + @property + def field_uncalib(self): + ''' + .. versionadded:: 1.3.1 + + Property that returns the current value of Uncalibrated Magnetic Field + (without hard iron calibration) along with the iron bias estimation + along the three axes. + ''' + return self.get_field_uncalib() + + def enable(self): + ''' + Activate the compass sensor. + ''' + self._enable() + + def disable(self): + ''' + Disable the compass sensor. + ''' + self._disable() + + def get_orientation(self): + return self._get_orientation() + + def get_field_uncalib(self): + ''' + .. versionadded:: 1.3.1 + ''' + return self._get_field_uncalib() + + # private + + def _enable(self): + raise NotImplementedError() + + def _disable(self): + raise NotImplementedError() + + def _get_orientation(self): + raise NotImplementedError() + + def _get_field_uncalib(self): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/cpu.py b/sbapp/plyer/facades/cpu.py new file mode 100644 index 0000000..3107560 --- /dev/null +++ b/sbapp/plyer/facades/cpu.py @@ -0,0 +1,92 @@ +''' +CPU +=== + +Simple Example +--------------- + +To get CPU count:: + >>> from plyer import cpu + >>> # 1 socket, 1 core per socket, 2 threads per core + >>> cpu.sockets # 1 CPU socket (or slot) + 1 + >>> cpu.physical # 1 CPU socket * 1 core per socket + 1 + >>> cpu.logical # 1 CPU socket * 1 core per socket * 2 threads per core + 2 + +Supported Platforms +------------------- + +MacOS +Linux +Windows +''' + + +class CPU: + ''' + Facade providing info about sockets, physical and logical + number of processors. + ''' + + @property + def sockets(self): + ''' + Property that contains the count of CPU sockets. + ''' + return self._sockets() + + @property + def physical(self): + ''' + Property that contains the total number of physical cores + (max core count) in the system. + + .. note:: `sockets * cores per socket` + ''' + return self._physical() + + @property + def logical(self): + ''' + Property that contains the total number of logical cores + (max thread count) in the system. + + .. note:: `sockets * cores per socket * threads per core` + ''' + return self._logical() + + @property + def cache(self): + ''' + Property that contains the count of L1, L2, L3 caches in the system + as a dictionary `{'L1': int, 'L2': int, 'L3': int}`. + ''' + return self._cache() + + @property + def numa(self): + ''' + Property that contains the count of NUMA nodes in the system. + + .. note:: https://en.wikipedia.org/wiki/Non-uniform_memory_access + ''' + return self._numa() + + # private + + def _sockets(self): + raise NotImplementedError() + + def _physical(self): + raise NotImplementedError() + + def _logical(self): + raise NotImplementedError() + + def _cache(self): + raise NotImplementedError() + + def _numa(self): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/devicename.py b/sbapp/plyer/facades/devicename.py new file mode 100644 index 0000000..b58ae58 --- /dev/null +++ b/sbapp/plyer/facades/devicename.py @@ -0,0 +1,44 @@ +'''DeviceName facade. + +Returns the following depending on the platform: + +* **Android**: Android Device name +* **Linux**: Hostname of the machine +* **OS X**: Hostname of the machine +* **Windows**: Hostname of the machine + +Simple Example +-------------- + +To get the Device Name:: + + >>> from plyer import devicename + >>> devicename.device_name + 'Oneplus 3' + +.. versionadded:: 2.1.0 + - first release + + +Supported Platforms +------------------- +Android, Windows, OS X, Linux + +''' + + +class DeviceName: + ''' + DeviceName facade. + ''' + + @property + def device_name(self): + ''' + Property that returns the device name of the platform. + ''' + return self._get_device_name() + + # private + def _get_device_name(self): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/email.py b/sbapp/plyer/facades/email.py new file mode 100644 index 0000000..c336046 --- /dev/null +++ b/sbapp/plyer/facades/email.py @@ -0,0 +1,58 @@ +''' +Email +===== + +The :class:`Email` provides access to public methods to use email of your +device. + +.. note:: + On Android `INTERNET` permission is needed. + +Simple Examples +--------------- + +To send an e-mail:: + + >>> from plyer import email + >>> recipient = 'abc@gmail.com' + >>> subject = 'Hi' + >>> text = 'This is an example.' + >>> create_chooser = False + >>> email.send(recipient=recipient, subject=subject, text=text, + create_chooser=create_chooser) + + >>> # opens email interface where user can change the content. + +Supported Platforms +------------------- +Android, iOS, Windows, OS X, Linux + +''' + + +class Email: + ''' + Email facade. + ''' + + def send(self, recipient=None, subject=None, text=None, + create_chooser=None): + ''' + Open an email client message send window, prepopulated with the + given arguments. + + :param recipient: Recipient of the message (str) + :param subject: Subject of the message (str) + :param text: Main body of the message (str) + :param create_chooser: Whether to display a program chooser to + handle the message (bool) + + .. note:: create_chooser is only supported on Android + ''' + self._send(recipient=recipient, subject=subject, text=text, + create_chooser=create_chooser) + + # private + + def _send(self, **kwargs): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/filechooser.py b/sbapp/plyer/facades/filechooser.py new file mode 100644 index 0000000..bfad4ae --- /dev/null +++ b/sbapp/plyer/facades/filechooser.py @@ -0,0 +1,74 @@ +''' +Native filechooser dialog facade. +================================= + +open_file, save_file and choose_dir accept a number of arguments +listed below. They return either a list of paths (normally +absolute), or None if no file was selected or the operation was +canceled and no result is available. + +Arguments: + * **path** *(string or None)*: a path that will be selected + by default, or None + * **multiple** *(bool)*: True if you want the dialog to + allow multiple file selection. (Note: Windows doesn't + support multiple directory selection) + * **filters** *(iterable)*: either a list of wildcard patterns + or of sequences that contain the name of the filter and any + number of wildcards that will be grouped under that name + (e.g. [["Music", "*mp3", "*ogg", "*aac"], "*jpg", "*py"]) + * **preview** *(bool)*: True if you want the file chooser to + show a preview of the selected file, if supported by the + back-end. + * **title** *(string or None)*: The title of the file chooser + window, or None for the default title. + * **icon** *(string or None)*: Path to the icon of the file + chooser window (where supported), or None for the back-end's + default. + * **show_hidden** *(bool)*: Force showing hidden files (currently + supported only on Windows) + * **on_selection** *(func)*: Callback for fetching the selection. + +Important: these methods will return only after user interaction. +Use threads or you will stop the mainloop if your app has one. + +.. versionchanged:: 1.4.0 + Added Android implementation for open_file() + Added ``on_selection`` kwarg for callback function + +Supported Plaforms +------------------ +Android, iOS, macOS, Linux, Windows +''' + + +class FileChooser: + ''' + File Chooser facade. + ''' + + def open_file(self, *args, **kwargs): + """ + Open the file chooser in "open" mode. + """ + return self._file_selection_dialog(mode="open", *args, **kwargs) + + def save_file(self, *args, **kwargs): + """ + Open the file chooser in "save" mode. Confirmation will be asked + when a file with the same name already exists. + """ + return self._file_selection_dialog(mode="save", *args, **kwargs) + + def choose_dir(self, *args, **kwargs): + """ + Open the directory chooser. Note that on Windows this is very + limited. Consider writing your own chooser if you target that + platform and are planning on using unsupported features. + """ + return self._file_selection_dialog(mode="dir", *args, **kwargs) + + # private + + def _file_selection_dialog(self, **kwargs): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/flash.py b/sbapp/plyer/facades/flash.py new file mode 100644 index 0000000..ac70b2e --- /dev/null +++ b/sbapp/plyer/facades/flash.py @@ -0,0 +1,77 @@ +# coding=utf-8 +''' +Flash +===== + +The :class:`Flash` provides access to public methods to use flash of your +device. + +.. note:: + In android you need CAMERA, FLASHLIGHT permissions + to access flash. + +.. versionadded:: 1.2.5 + +This can be used to activate the flash of your camera on +Android and iOS. + +Simple Examples +--------------- + +To turn on flash:: + + >>> from plyer import flash + >>> flash.on() + +To turn off flash:: + + >>> flash.off() + +To release flash:: + + >>> flash.release() + +Supported Platforms +------------------- +Android, iOS + +''' + + +class Flash: + """ + Flash facade. + """ + + def on(self): + """ + Activate the flash + """ + self._on() + + def off(self): + """ + Deactiavte the flash + """ + self._off() + + def release(self): + """ + Release any access to the Flash / Camera. + Call this when you're done using the Flash. + This will release the Camera, and stop any process. + + Next call to `_on` will reactivate it. + """ + self._release() + + # private + + def _on(self): + raise NotImplementedError() + + def _off(self): + raise NotImplementedError() + + def _release(self): + pass diff --git a/sbapp/plyer/facades/gps.py b/sbapp/plyer/facades/gps.py new file mode 100644 index 0000000..9fc200b --- /dev/null +++ b/sbapp/plyer/facades/gps.py @@ -0,0 +1,96 @@ +''' +GPS +==== + +.. versionadded:: 1.1 + +.. note:: + On Android `INTERNET`, `ACCESS_FINE_LOCATION`, `ACCESS_COARSE_LOCATION` + permissions are needed. + + +.. note:: + On iOS `NSLocationWhenInUseUsageDescription` key is required for app to + display geolocation usage permission prompt. Key can be added in Xcode + target `info` section or in ``Resources/-info.plist``. + App background mode (`on_pause`) also must be supported. + +You need to set a `on_location` callback with the :meth:`configure` method. +This callback will receive a couple of keywords / values, that might be +different depending of their availability on the targeted platform. +Lat and lon are always available. + +- lat: latitude of the last location, in degrees +- lon: longitude of the last location, in degrees +- speed: speed of the user, in meters/second over ground +- bearing: bearing in degrees +- altitude: altitude in meters above the sea level + +Here is an example of the usage of gps:: + + from plyer import gps + + def print_locations(**kwargs): + print 'lat: {lat}, lon: {lon}'.format(**kwargs) + + gps.configure(on_location=print_locations) + gps.start() + # later + gps.stop() + +Supported Platforms +------------------- +Android, iOS + +''' + + +class GPS: + ''' + GPS facade. + ''' + + def configure(self, on_location, on_status=None): + ''' + Configure the GPS object. This method should be called before + :meth:`start`. + + :param on_location: Function to call when receiving a new location + :param on_status: Function to call when a status message is received + :type on_location: callable, multiples keys/value will be passed. + :type on_status: callable, args are "message-type", "status" + + .. warning:: + + The `on_location` and `on_status` callables might be called from + another thread than the thread used for creating the GPS object. + ''' + self.on_location = on_location + self.on_status = on_status + self._configure() + + def start(self, minTime=1000, minDistance=1): + ''' + Start the GPS location updates. + Expects 2 parameters: + minTime: milliseconds. (float) + minDistance: meters. (float) + ''' + self._start(minTime=minTime, minDistance=minDistance) + + def stop(self): + ''' + Stop the GPS location updates. + ''' + self._stop() + + # private + + def _configure(self): + raise NotImplementedError() + + def _start(self, **kwargs): + raise NotImplementedError() + + def _stop(self): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/gravity.py b/sbapp/plyer/facades/gravity.py new file mode 100644 index 0000000..d6e2695 --- /dev/null +++ b/sbapp/plyer/facades/gravity.py @@ -0,0 +1,38 @@ +class Gravity: + '''Gravity facade. + + .. versionadded:: 1.2.5 + + Supported Platforms:: Android + + ''' + + @property + def gravity(self): + '''Property that returns values of the current gravity force + as a (x, y, z) tuple. Returns (None, None, None) + if no data is currently available. + ''' + return self._get_gravity() + + def enable(self): + '''Activate the gravity sensor. Throws an error if the + hardware is not available or not implemented on. + ''' + self._enable() + + def disable(self): + '''Disable the gravity sensor. + ''' + self._disable() + + # private + + def _enable(self): + raise NotImplementedError() + + def _disable(self): + raise NotImplementedError() + + def _get_gravity(self): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/gyroscope.py b/sbapp/plyer/facades/gyroscope.py new file mode 100644 index 0000000..695f168 --- /dev/null +++ b/sbapp/plyer/facades/gyroscope.py @@ -0,0 +1,126 @@ +''' +Gyroscope +============ + +The gyroscope measures the rate of rotation around a device's x, y, +and z axis. + +The :class:`Gyroscope` provides access to public methods to +use gyroscope of your device. + +Simple Examples +--------------- + +To enable gyroscope:: + + >>> from plyer import gyroscope + >>> gyroscope.enable() + +To disable gyroscope:: + + >>> gyroscope.disable() + +To get the rate of rotation along the three axes:: + + >>> gyroscope.rotation + (-0.0034587313421070576, -0.0073830625042319298, 0.0046892408281564713) + +To get the uncalibrated rate of rotation along the three axes along with the +drift compensation:: + + >>> gyroscope.rotation_uncalib + () + where the first three values show the rate of rotation w/o drift + compensation and the last three show the estimated drift along the three + axes. + +Supported Platforms +------------------- +Android, iOS + +''' + + +class Gyroscope: + ''' + Gyroscope facade. + + .. versionadded:: 1.3.1 + ''' + + @property + def rotation(self): + ''' + Property that returns the rate of rotation around the device's local + X, Y and Z axis. + + Along x-axis: angular speed around the X axis + Along y-axis: angular speed around the Y axis + Along z-axis: angular speed around the Z axis + + Returns (None, None, None) if no data is currently available. + ''' + return self.get_orientation() + + @property + def rotation_uncalib(self): + ''' + Property that returns the current rate of rotation around the X, Y and + Z axis. An estimation of the drift on each axis is reported as well. + + Along x-axis: angular speed (w/o drift compensation) around the X axis + Along y-axis: angular speed (w/o drift compensation) around the Y axis + Along z-axis: angular speed (w/o drift compensation) around the Z axis + + Along x-axis: estimated drift around X axis + Along y-axis: estimated drift around Y axis + Along z-axis: estimated drift around Z axis + + Returns (None, None, None, None, None, None) if no data is currently + available. + ''' + return self.get_rotation_uncalib() + + @property + def orientation(self): + ''' + WARNING:: This property is deprecated after API Level 8. + Use `gyroscope.rotation` instead. + + Property that returns values of the current Gyroscope sensors, as + a (x, y, z) tuple. Returns (None, None, None) if no data is currently + available. + ''' + return self.get_orientation() + + def enable(self): + ''' + Activate the Gyroscope sensor. + ''' + self._enable() + + def disable(self): + ''' + Disable the Gyroscope sensor. + ''' + self._disable() + + def get_orientation(self): + return self._get_orientation() + + def get_rotation_uncalib(self): + return self._get_rotation_uncalib() + + # private + + def _enable(self): + raise NotImplementedError() + + def _disable(self): + raise NotImplementedError() + + def _get_orientation(self): + raise NotImplementedError() + + def _get_rotation_uncalib(self): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/humidity.py b/sbapp/plyer/facades/humidity.py new file mode 100644 index 0000000..ac63333 --- /dev/null +++ b/sbapp/plyer/facades/humidity.py @@ -0,0 +1,34 @@ +class Humidity: + '''Humidity facade. + Humidity sensor returns value of humidity. + With method `enable` you can turn on Humidity sensor and + 'disable' method stops the sensor. + Use property `tell` to get humidity value. + + Supported Platforms + ------------------- + Android + ''' + + @property + def tell(self): + '''Current humidity''' + return self._get_humidity() + + def enable(self): + '''Enable Humidity sensor.''' + self._enable() + + def disable(self): + '''Disable Humidity sensor.''' + self._disable() + + # private + def _get_humidity(self, **kwargs): + raise NotImplementedError() + + def _enable(self, **kwargs): + raise NotImplementedError() + + def _disable(self, **kwargs): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/irblaster.py b/sbapp/plyer/facades/irblaster.py new file mode 100644 index 0000000..a6263cd --- /dev/null +++ b/sbapp/plyer/facades/irblaster.py @@ -0,0 +1,104 @@ +''' +IrBlaster +============ + +The :class:`IrBlaster` provides access to public methods by which your device +can act as a remote and could be used to control your TV, AC, Music Player, +Projectors, Set top box or anything that can be controlled by a remote. + +.. note:: + - On Android your app needs the TRANSMIT_IR permission which allows an + application to use the device's IR transmitter, If available. + +Simple Examples +--------------- + +To get transmit an IR sequence:: + + >>> from plyer import irblaster + >>> irblaster.transmit(frequency, pattern, mode) + +To get frequencies:: + + >>> irblaster.frequencies + +To check if IrBlaster exists:: + + >>> irblaster.exists() + True/False + +Supported Platforms +------------------- +Android + +''' + + +class IrBlaster: + ''' + Infrared blaster facade. + ''' + + @staticmethod + def periods_to_microseconds(frequency, pattern): + ''' + Convert a pattern from period counts to microseconds. + ''' + period = 1000000. / frequency + return [period * x for x in pattern] + + @staticmethod + def microseconds_to_periods(frequency, pattern): + ''' + Convert a pattern from microseconds to period counts. + ''' + period = 1000000. / frequency + return [x / period for x in pattern] + + @property + def frequencies(self): + ''' + Property which contains a list of frequency ranges + supported by the device in the form: + + [(from1, to1), + (from2, to2), + ... + (fromN, toN)] + ''' + return self.get_frequencies() + + def get_frequencies(self): + return self._get_frequencies() + + def transmit(self, frequency, pattern, mode='period'): + ''' + Transmit an IR sequence. + + :parameters: + `frequency`: int + Carrier frequency for the IR transmission. + `pattern`: list[int] + Burst pair pattern to transmit. + `mode`: str, defaults to 'period' + Specifies the format of the pattern values. + Can be 'period' or 'microseconds'. + ''' + return self._transmit(frequency, pattern, mode) + + def exists(self): + ''' + Check if the device has an infrared emitter. + ''' + return self._exists() + + # private + + def _get_frequencies(self): + raise NotImplementedError() + + def _transmit(self, frequency, pattern, mode): + raise NotImplementedError() + + def _exists(self): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/keystore.py b/sbapp/plyer/facades/keystore.py new file mode 100644 index 0000000..5d0f21d --- /dev/null +++ b/sbapp/plyer/facades/keystore.py @@ -0,0 +1,32 @@ +''' +Keystore +======= +The :class:`Keystore` provides a mechanism for securing/storing +cryptographic keys (such as user credentials) in a container. +Typically needed to support authentication APIs such as OAuth2 +.. note:: + Typically needed to support authentication APIs such as OAuth2 + +Supported Platforms +------------------- +Android, iOS, Windows, OS X, Linux +--------------- +''' + + +class Keystore: + ''' + Keystore facade + ''' + + def set_key(self, servicename, key, value, **kwargs): + self._set_key(servicename, key, value, **kwargs) + + def _set_key(self, servicename, key, value, **kwargs): + raise NotImplementedError() + + def get_key(self, servicename, key, **kwargs): + return self._get_key(servicename, key) + + def _get_key(self, servicename, key, **kwargs): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/light.py b/sbapp/plyer/facades/light.py new file mode 100644 index 0000000..498e28e --- /dev/null +++ b/sbapp/plyer/facades/light.py @@ -0,0 +1,39 @@ +class Light: + '''Light facade. + + Light sensor measures the ambient light level(illumination) in lx. + Common uses include controlling screen brightness. + + With method `enable` you can turn on the sensor and + `disable` method stops the sensor. + + Use property `illumination` to get current illumination in lx. + + .. versionadded:: 1.2.5 + + Supported Platforms:: Android + ''' + + @property + def illumination(self): + '''Current illumination in lx.''' + return self._get_illumination() + + def enable(self): + '''Enable light sensor.''' + self._enable() + + def disable(self): + '''Disable light sensor.''' + self._disable() + + # private + + def _get_illumination(self, **kwargs): + raise NotImplementedError() + + def _enable(self, **kwargs): + raise NotImplementedError() + + def _disable(self, **kwargs): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/notification.py b/sbapp/plyer/facades/notification.py new file mode 100644 index 0000000..5b09dee --- /dev/null +++ b/sbapp/plyer/facades/notification.py @@ -0,0 +1,93 @@ +''' +Notification +============ + +The :class:`Notification` provides access to public methods to create +notifications. + +Simple Examples +--------------- + +To send notification:: + + >>> from plyer import notification + >>> title = 'plyer' + >>> message = 'This is an example.' + >>> notification.notify(title=title, message=message) + +Android toast notification:: + + >>> from plyer import notification + >>> notification.notify(message='hello', toast=True) + +Android simple notification:: + + >>> from plyer import notification + >>> notification.notify(message='hello', toast=True) + +Notification with custom icon:: + + >>> from plyer import notification + >>> notification.notify(title='title', message='hello', app_icon=) + +.. versionadded:: 1.0.0 + +.. versionadded:: 1.4.0 + Add implementation of primitive Android popup-like notification (toast) + +.. versionchanged:: 1.4.0 + Android implementation now supports custom icons for notifications. +''' + + +class Notification: + ''' + Notification facade. + ''' + + def notify(self, title='', message='', app_name='', app_icon='', notification_icon=None, + timeout=10, ticker='', toast=False, hints={}): + ''' + Send a notification. + + :param title: Title of the notification + :param message: Message of the notification + :param app_name: Name of the app launching this notification + :param app_icon: Icon to be displayed along with the message + :param timeout: time to display the message for, defaults to 10 + :param ticker: text to display on status bar as the notification + arrives + :param toast: simple Android message instead of full notification + :param hints: Optional hints that can be used to pass along extra + instructions on Linux. + (See https://specifications.freedesktop.org/notification-spec/latest/ar01s08.html) # noqa: E501 + + :type title: str + :type message: str + :type app_name: str + :type app_icon: str + :type timeout: int + :type ticker: str + :type toast: bool + :type hints: dict + + .. note:: + When called on Windows, ``app_icon`` has to be a path to + a file in .ICO format. + + .. versionadded:: 1.0.0 + + .. versionchanged:: 1.4.0 + Add 'toast' keyword argument + ''' + + self._notify( + title=title, message=message, + app_icon=app_icon, app_name=app_name, notification_icon=notification_icon, + timeout=timeout, ticker=ticker, toast=toast, hints=hints + ) + + # private + + def _notify(self, **kwargs): + raise NotImplementedError("No usable implementation found!") diff --git a/sbapp/plyer/facades/orientation.py b/sbapp/plyer/facades/orientation.py new file mode 100644 index 0000000..56ea0ba --- /dev/null +++ b/sbapp/plyer/facades/orientation.py @@ -0,0 +1,81 @@ +''' +Orientation +========== + +The :class:`Orientation` provides access to public methods to set orientation +of your device. + +.. note:: + These settings are generally guidelines, the operating + system may choose to ignore them, or they may be overridden by + other system components. + +.. versionadded:: 1.2.4 + +Simple Examples +--------------- + +To set landscape:: + + >>> from plyer import orientation + >>> orientation.set_landscape() + +To set portrait:: + + >>> orientation.set_portrait() + +To set sensor:: + + >>> orientation.set_sensor() + +Supported Platforms +------------------- +Android + +''' + + +class Orientation: + ''' + Orientation facade. + ''' + + def set_landscape(self, reverse=False): + ''' + Rotate the app to a landscape orientation. + + :param reverse: If True, uses the opposite of the natural + orientation. + ''' + self._set_landscape(reverse=reverse) + + def set_portrait(self, reverse=False): + ''' + Rotate the app to a portrait orientation. + + :param reverse: If True, uses the opposite of the natural + orientation. + ''' + self._set_portrait(reverse=reverse) + + def set_sensor(self, mode='any'): + ''' + Rotate freely following sensor information from the device. + + :param mode: The rotation mode, should be one of 'any' (rotate + to any orientation), 'landscape' (choose nearest + landscape mode) or 'portrait' (choose nearest + portrait mode). Defaults to 'any'. + ''' + self._set_sensor(mode=mode) + + # private + + def _set_landscape(self, **kwargs): + raise NotImplementedError() + + def _set_portrait(self, **kwargs): + raise NotImplementedError() + + def _set_sensor(self, **kwargs): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/processors.py b/sbapp/plyer/facades/processors.py new file mode 100644 index 0000000..04b7440 --- /dev/null +++ b/sbapp/plyer/facades/processors.py @@ -0,0 +1,42 @@ +''' +Number of Processors +======= +The :class:`Processors` provides a information on the number of +processors in a system +.. note:: + Deprecated in favor of `cpu` + +Simple Example +--------------- +To get processors status:: + >>> from plyer import processors + >>> processors.status + {'Number_of_Processors': '4'} +Supported Platforms +------------------- +Linux +''' + + +class Processors: + ''' + Number of Processors info facade. + ''' + + @property + def status(self): + ''' + Property that contains a dict with the following fields: + * **Number_of_Processors** *(int)*: Number of Processors in + the system + .. warning:: + If any of the fields is not readable, it is set as + None. + ''' + return self.get_state() + + def get_state(self): + return self._get_state() + + def _get_state(self): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/proximity.py b/sbapp/plyer/facades/proximity.py new file mode 100644 index 0000000..d0fc303 --- /dev/null +++ b/sbapp/plyer/facades/proximity.py @@ -0,0 +1,44 @@ +class Proximity: + '''Proximity facade. + + The proximity sensor is commonly used to determine distance whether + phone is close to your head. Commonly is used when you have a call + and you stick your phone with your head. Then screen of phone turns off. + + Use method `enable` to turn on proximity sensor and method `disable` for + turn off. + + To check if some object (or your head) is near sensor check values from + property `proximity`. It returns `True` when object is close. + + .. versionadded:: 1.2.5 + + Supported Platforms::Android + ''' + + @property + def proximity(self): + '''Return True or False depending if there is an object or not. + + :return: True if there is an object. Otherwise False. + ''' + return self._get_proximity() + + def _enable(self, **kwargs): + raise NotImplementedError() + + def enable(self): + '''Enable the proximity sensor. + ''' + self._enable() + + def _disable(self, **kwargs): + raise NotImplementedError() + + def disable(self): + '''Disable the proximity sensor. + ''' + self._disable() + + def _get_proximity(self): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/screenshot.py b/sbapp/plyer/facades/screenshot.py new file mode 100644 index 0000000..ddfb293 --- /dev/null +++ b/sbapp/plyer/facades/screenshot.py @@ -0,0 +1,57 @@ +''' +Screenshot +========== + +The :class:`Screenshot` is used for capturing a digital image of what +is currently visible on the monitor. + +The default path for taking screenshot is set in each platform implementation. + +Simple Examples +--------------- + +To get the file path:: + + >>> screenshot.file_path + '/sdcard/test.jpg' + +To set the file path:: + + >>> screenshot.file_path = '/Users/OSXUser/Pictures/screenshot.png' + +To take screenshot:: + + >>> from plyer import screenshot + >>> screenshot.capture() +''' + + +class Screenshot: + ''' + Screenshot facade. + ''' + + _file_path = '' + + def __init__(self, file_path=None): + self._file_path = file_path + + def capture(self): + self._capture() + + @property + def file_path(self): + return self._file_path + + @file_path.setter + def file_path(self, location): + ''' + Location of the screenshot. + ''' + + self._file_path = location + + # private + + def _capture(self, **kwargs): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/sms.py b/sbapp/plyer/facades/sms.py new file mode 100644 index 0000000..0e58f44 --- /dev/null +++ b/sbapp/plyer/facades/sms.py @@ -0,0 +1,51 @@ +''' +Sms +==== + +The :class:`Sms` provides access to sending Sms from your device. + +.. note:: + + On Android your app needs the SEND_SMS permission in order to + send sms messages. + +.. versionadded:: 1.2.0 + +Simple Examples +--------------- + +To send sms:: + + >>> from plyer import sms + >>> recipient = 9999222299 + >>> message = 'This is an example.' + >>> sms.send(recipient=recipient, message=message) + +Supported Platforms +------------------- +Android, iOS + +''' + + +class Sms: + ''' + Sms facade. + ''' + + def send(self, recipient, message): + ''' + Send SMS or open SMS interface. + + :param recipient: The receiver + :param message: the message + + :type recipient: number + :type message: str + ''' + self._send(recipient=recipient, message=message) + + # private + + def _send(self, **kwargs): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/spatialorientation.py b/sbapp/plyer/facades/spatialorientation.py new file mode 100644 index 0000000..9fe400b --- /dev/null +++ b/sbapp/plyer/facades/spatialorientation.py @@ -0,0 +1,54 @@ +# coding=utf-8 + + +class SpatialOrientation: + '''Spatial Orientation facade. + + Computes the device's orientation based on the rotation matrix. + + .. versionadded:: 1.3.1 + ''' + + @property + def orientation(self): + '''Property that returns values of the current device orientation + as a (azimuth, pitch, roll) tuple. + + Azimuth, angle of rotation about the -z axis. This value represents the + angle between the device's y axis and the magnetic north pole. + The range of values is -π to π. + + Pitch, angle of rotation about the x axis. This value represents the + angle between a plane parallel to the device's screen and a plane + parallel to the ground. + The range of values is -π to π. + + Roll, angle of rotation about the y axis. This value represents the + angle between a plane perpendicular to the device's screen and a plane + perpendicular to the ground. + The range of values is -π/2 to π/2. + + Returns (None, None, None) if no data is currently available. + + Supported Platforms:: Android + ''' + return self._get_orientation() or (None, None, None) + + def _get_orientation(self): + raise NotImplementedError() + + def enable_listener(self): + '''Enable the orientation sensor. + ''' + self._enable_listener() + + def _enable_listener(self, **kwargs): + raise NotImplementedError() + + def disable_listener(self): + '''Disable the orientation sensor. + ''' + self._disable_listener() + + def _disable_listener(self, **kwargs): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/storagepath.py b/sbapp/plyer/facades/storagepath.py new file mode 100644 index 0000000..7445a56 --- /dev/null +++ b/sbapp/plyer/facades/storagepath.py @@ -0,0 +1,135 @@ +''' +Storage Path +============ + +The StorgePath API can be used to gain access to standard storage locations +across platforms such as home directory, root directory, external storage +directory, documents, downloads, etc. + +The :class:`StoragePath` provides access to public methods to access standard +storage locations. + +Simple Examples +--------------- + +To get the path of user's home directory:: + + >>> from plyer import storagepath + >>> storagepath.get_home_dir() + +To get the path of standard downloads directory:: + + >>> from plyer import storagepath + >>> storagepath.get_downloads_dir() + +To get the path of directory holding application files:: + + >>> from plyer import storagepath + >>> storagepath.get_application_dir() + +''' + + +class StoragePath: + ''' + StoragePath facade. + ''' + + def get_home_dir(self): + ''' + Get the path of home directory of current user. + ''' + return self._get_home_dir() + + def get_external_storage_dir(self): + ''' + Get the path of primary shared or external storage directory. + ''' + return self._get_external_storage_dir() + + def get_sdcard_dir(self): + ''' + Get the path of external SD card. + + .. versionadded:: 1.4.0 + ''' + return self._get_sdcard_dir() + + def get_root_dir(self): + ''' + Get the path of root of the "system" partition holding the core OS. + ''' + return self._get_root_dir() + + def get_documents_dir(self): + ''' + Get the path of standard directory in which to place documents that + have been created by the user. + ''' + return self._get_documents_dir() + + def get_downloads_dir(self): + ''' + Get the path of standard directory in which to place files that have + been downloaded by the user. + ''' + return self._get_downloads_dir() + + def get_videos_dir(self): + ''' + Get the path of standard directory in which to place videos that are + available to the user. + ''' + return self._get_videos_dir() + + def get_music_dir(self): + ''' + Get the path of standard directory in which to place any audio files + that should be in the regular list of music for the user. + ''' + return self._get_music_dir() + + def get_pictures_dir(self): + ''' + Standard directory in which to place pictures that are available to + the user. + ''' + return self._get_pictures_dir() + + def get_application_dir(self): + ''' + Get the path of the directory holding application files. + ''' + return self._get_application_dir() + + # private + + def _get_home_dir(self): + raise NotImplementedError() + + def _get_external_storage_dir(self): + raise NotImplementedError() + + def _get_sdcard_dir(self): + raise NotImplementedError() + + def _get_root_dir(self): + raise NotImplementedError() + + def _get_documents_dir(self): + raise NotImplementedError() + + def _get_downloads_dir(self): + raise NotImplementedError() + + def _get_videos_dir(self): + raise NotImplementedError() + + def _get_music_dir(self): + raise NotImplementedError() + + def _get_pictures_dir(self): + raise NotImplementedError() + + def _get_application_dir(self): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/stt.py b/sbapp/plyer/facades/stt.py new file mode 100644 index 0000000..9306df6 --- /dev/null +++ b/sbapp/plyer/facades/stt.py @@ -0,0 +1,188 @@ +''' +Speech to Text +============== + +.. versionadded:: 1.4.0 + +Speech Recognition facade. + +In order to check that your device supports voice recognition use method +`exist`. + +Variable `language` indicates which language will be used to match words from + voice. + +Use `start` to start voice recognition immediately and `stop` to stop. + +.. note:: + Needed permissions for Android: `RECORD_AUDIO` (and `INTERNET` if you want + online voice recognition API to be used) + +.. note:: + On Android platform, after execute `start` method you can hear BEEP! + Mute sound in order to disable it. + +.. note:: + For Android implementation to work there has to be an application with + `android.speech.RecognitionService` implementation present in the system. + Mostly it's `com.google.android.googlequicksearchbox` or "Google" + application (the search bar with the launcher widget). + +Offline Speech Recognition on Android +------------------------------------- + +Requires any application that provides an +`android.speech.RecognitionService` implementation to the other apps. One of +such applications is on a lot of devices preinstalled Google (quick search +box). + +The API prefers offline recognition, but should be able to switch to online +alternative in case you don't have a language package installed (`INTERNET` +permission necessary). + +You can enable offline speech recognition this way (Android 8.1): + +* open the `Settings` app +* choose `Language & Input` / `Language & Keyboard` (Samsung might include it + in the `General` category) +* choose `On-Screen keyboard` or `Voice search` +* choose `Google Keyboard` +* choose `Offline Speech recognition` +* download language package if you don't have one already + +Simple Examples +--------------- + +To start listening:: + + >>> from plyer import stt + >>> stt.start() + +To retrieve partial results while listening:: + + >>> assert stt.listening + >>> print(stt.partial_results) + +To stop listening:: + + >>> stt.stop() + +To retrieve results after the listening stopped:: + + >>> print(stt.results) +''' + + +class STT: + ''' + Speech to text facade. + ''' + + _language = 'en-US' + ''' + Default language in which platform will try to recognize voice. + In order to change language pick one from list by using + `supported_languages` method. + ''' + + _supported_languages = [ + 'en-US', + 'pl-PL' + ] + + results = [] + ''' + List of sentences found while listening. It may consist of many similar + and possible sentences that was recognition program. + ''' + + errors = [] + ''' + List of errors found while listening. + ''' + + partial_results = [] + ''' + List of results found while the listener is still being active. + ''' + + prefer_offline = True + ''' + Preference whether to use offline language package necessary for + each platform dependant implementation or online API. + ''' + + listening = False + ''' + Current state of listening. + ''' + + @property + def supported_languages(self): + ''' + Return list of supported languages used in recognition. + ''' + + return self._supported_languages + + @property + def language(self): + ''' + Return current language. + ''' + + return self._language + + @language.setter + def language(self, lang): + ''' + Set current language. + + Value can not be set if it's not supported. See `supported_languages` + to get what language you can set. + + .. note:: + We obviously can't check each language, therefore if you find + that a specific language is available to you and the only limitation + is our check for the internally defined `supported_languages`, feel + free to open a pull request for adding your language to the list. + ''' + + if lang in self.supported_languages: + self._language = lang + + # public methods + def start(self): + ''' + Start listening. + ''' + + self.results = [] + self.partial_results = [] + self._start() + self.listening = True + + def stop(self): + ''' + Stop listening. + ''' + + self._stop() + self.listening = False + + def exist(self): + ''' + Returns a boolean for speech recognition availability. + ''' + + return self._exist() + + # private methods + def _start(self): + raise NotImplementedError + + def _stop(self): + raise NotImplementedError + + def _exist(self): + raise NotImplementedError diff --git a/sbapp/plyer/facades/temperature.py b/sbapp/plyer/facades/temperature.py new file mode 100644 index 0000000..baaae2a --- /dev/null +++ b/sbapp/plyer/facades/temperature.py @@ -0,0 +1,41 @@ +# coding=utf-8 + + +class Temperature: + '''Temperature facade. + + Temperature sensor is used to measure the ambient room temperature in + degrees Celsius (°C) + With method `enable` you can turn on temperature sensor and 'disable' + method stops the sensor. + Use property `temperature` to get ambient air temperature in degree C. + + .. versionadded:: 1.2.5 + + Supported Platforms:: Android + + ''' + + @property + def temperature(self): + '''Current air temperature in degree C.''' + return self._get_temperature() + + def enable(self): + '''Enable temperature sensor.''' + self._enable() + + def disable(self): + '''Disable temperature sensor.''' + self._disable() + + # private + + def _get_temperature(self, **kwargs): + raise NotImplementedError() + + def _enable(self, **kwargs): + raise NotImplementedError() + + def _disable(self, **kwargs): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/tts.py b/sbapp/plyer/facades/tts.py new file mode 100644 index 0000000..c4fd366 --- /dev/null +++ b/sbapp/plyer/facades/tts.py @@ -0,0 +1,39 @@ +''' +TTS +==== + +The :class:`TTS` provides provides access to public methods to +use text to speech of your device. + +Simple Examples +--------------- + +To speak:: + + >>> from plyer import tts + >>> tts.speak(message=message) + +Supported Platforms +------------------- +Android, iOS, Windows, OS X, Linux + +''' + + +class TTS: + ''' + TextToSpeech facade. + ''' + + def speak(self, message=''): + '''Use text to speech capabilities to speak the message. + + :param message: What to speak + :type message: str + ''' + self._speak(message=message) + + # private + + def _speak(self, **kwargs): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/uniqueid.py b/sbapp/plyer/facades/uniqueid.py new file mode 100644 index 0000000..07630e3 --- /dev/null +++ b/sbapp/plyer/facades/uniqueid.py @@ -0,0 +1,54 @@ +'''UniqueID facade. + +Returns the following depending on the platform: + +* **Android**: Android ID +* **OS X**: Serial number of the device +* **Linux**: Serial number using lshw +* **Windows**: MachineGUID from regkey +* **iOS**: UUID + +Simple Example +-------------- + +To get the unique ID:: + + >>> from plyer import uniqueid + >>> uniqueid.id + '1b1a7a4958e2a845' + +.. versionadded:: 1.2.0 + +.. versionchanged:: 1.2.4 + On Android returns Android ID instead of IMEI. + +Supported Platforms +------------------- +Android, iOS, Windows, OS X, Linux + +''' + + +class UniqueID: + ''' + UniqueID facade. + ''' + + @property + def id(self): + ''' + Property that returns the unique id of the platform. + ''' + return self.get_uid() + + def get_uid(self): + ''' + Public method for receiving unique ID via platform-specific + API in plyer.platforms. + ''' + return self._get_uid() + + # private + + def _get_uid(self): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/vibrator.py b/sbapp/plyer/facades/vibrator.py new file mode 100644 index 0000000..345550c --- /dev/null +++ b/sbapp/plyer/facades/vibrator.py @@ -0,0 +1,97 @@ +''' +Vibrator +======= + +The :class:`Vibrator` provides access to public methods to use vibrator of your +device. + +.. note:: + On Android your app needs the VIBRATE permission to + access the vibrator. + +Simple Examples +--------------- + +To vibrate your device:: + + >>> from plyer import vibrator + >>> time=2 + >>> vibrator.vibrate(time=time) + +To set a pattern:: + + >>> vibrator.pattern(pattern=pattern, repeat=repeat) + +To know whether vibrator exists or not:: + + >>> vibrator.exists() + +To cancel vibration:: + + >>> vibrator.cancel() + +Supported Platforms +------------------- +Android, iOS + + +''' + + +class Vibrator: + ''' + Vibration facade. + ''' + + def vibrate(self, time=1): + ''' + Ask the vibrator to vibrate for the given period. + + :param time: Time to vibrate for, in seconds. Default is 1. + ''' + self._vibrate(time=time) + + def pattern(self, pattern=(0, 1), repeat=-1): + ''' + Ask the vibrator to vibrate with the given pattern, with an + optional repeat. + + :param pattern: Pattern to vibrate with. Should be a list of + times in seconds. The first number is how long to wait + before vibrating, and subsequent numbers are times to + vibrate and not vibrate alternately. + Defaults to ``[0, 1]``. + + :param repeat: Index at which to repeat the pattern. When the + vibration pattern reaches this index, it will start again + from the beginning. Defaults to ``-1``, which means no + repeat. + ''' + self._pattern(pattern=pattern, repeat=repeat) + + def exists(self): + ''' + Check if the device has a vibrator. Returns True or + False. + ''' + return self._exists() + + def cancel(self): + ''' + Cancels any current vibration, and stops the vibrator. + ''' + self._cancel() + + # private + + def _vibrate(self, **kwargs): + raise NotImplementedError() + + def _pattern(self, **kwargs): + raise NotImplementedError() + + def _exists(self, **kwargs): + raise NotImplementedError() + + def _cancel(self, **kwargs): + raise NotImplementedError() diff --git a/sbapp/plyer/facades/wifi.py b/sbapp/plyer/facades/wifi.py new file mode 100644 index 0000000..5f0742f --- /dev/null +++ b/sbapp/plyer/facades/wifi.py @@ -0,0 +1,187 @@ +''' +Wifi Facade. +============= + +The :class:`Wifi` is to provide access to the wifi of your mobile/ desktop +devices. + +It currently supports `connecting`, `disconnecting`, `scanning`, `getting +available wifi network list` and `getting network information`. + +Simple examples +--------------- + +To enable/ turn on wifi scanning:: + + >>> from plyer import wifi + >>> wifi.start_scanning() + +Once the wifi is enabled/ turned on, then this command starts to scan +all the nearby available wifi networks. + +To get network info:: + + >>> from plyer import wifi + >>> wifi.start_scanning() + >>> return wifi.get_network_info(name) + +Returns network details of the network who's name/ssid is provided in the +`name` parameter. + +To connect to a network:: + + >>> from plyer import wifi + >>> wifi.start_scanning() + >>> wifi.connect(network, parameters) + +This connects to the network who's name/ssid is provided under `network` +parameter and along with other necessary methods for connection +which depends upon platform to platform. + +please visit following files for more details about requirements of +`paramaters` argument in `connect` method: + + plyer/platforms/win/wifi.py + plyer/platforms/macosx/wifi.py + plyer/platforms/win/wifi.py + +To disconnect from wifi:: + + >>> from plyer import wifi + >>> wifi.disconnect() + +This disconnects your device from any wifi network. + +To get available wifi networks:: + + >>> from plyer import wifi + >>> wifi.start_scanning() + >>> return wifi.get_available_wifi() + +This returns all the available wifi networks near the device. + +Supported Platforms +------------------- +Windows, OS X, Linux + +Ex: 6 +---------- + +from plyer import wifi +wifi.enable() + +This enables wifi device. + +Ex: 7 +---------- + +from plyer import wifi +wifi.disable() + +This disable wifi device +''' + + +class Wifi: + ''' + Wifi Facade. + ''' + + def is_enabled(self): + ''' + Return enabled status of WiFi hardware. + ''' + return self._is_enabled() + + def is_connected(self, interface=None): + ''' + Return connection state of WiFi interface. + + .. versionadded:: 1.4.0 + ''' + return self._is_connected(interface=interface) + + @property + def interfaces(self): + ''' + List all available WiFi interfaces. + + .. versionadded:: 1.4.0 + ''' + + raise NotImplementedError() + + def start_scanning(self, interface=None): + ''' + Turn on scanning. + ''' + return self._start_scanning(interface=interface) + + def get_network_info(self, name): + ''' + Return a dictionary of specified network. + ''' + return self._get_network_info(name=name) + + def get_available_wifi(self): + ''' + Returns a list of all the available wifi. + ''' + return self._get_available_wifi() + + def connect(self, network, parameters, interface=None): + ''' + Method to connect to some network. + ''' + self._connect( + network=network, + parameters=parameters, + interface=interface + ) + + def disconnect(self, interface=None): + ''' + To disconnect from some network. + ''' + self._disconnect(interface=interface) + + def enable(self): + ''' + Wifi interface power state is set to "ON". + ''' + self._enable() + + def disable(self): + ''' + Wifi interface power state is set to "OFF". + ''' + self._disable() + + # private + + def _is_enabled(self): + raise NotImplementedError() + + def _is_connected(self, interface=None): + raise NotImplementedError() + + def _start_scanning(self, interface=None): + raise NotImplementedError() + + def _get_network_info(self, **kwargs): + raise NotImplementedError() + + def _get_available_wifi(self): + raise NotImplementedError() + + def _connect(self, **kwargs): + raise NotImplementedError() + + def _disconnect(self, interface=None): + raise NotImplementedError() + + def _enable(self): + raise NotImplementedError() + + def _disable(self): + raise NotImplementedError() diff --git a/sbapp/plyer/platforms/__init__.py b/sbapp/plyer/platforms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sbapp/plyer/platforms/android/__init__.py b/sbapp/plyer/platforms/android/__init__.py new file mode 100644 index 0000000..8fe1263 --- /dev/null +++ b/sbapp/plyer/platforms/android/__init__.py @@ -0,0 +1,18 @@ +from os import environ +from jnius import autoclass + +ANDROID_VERSION = autoclass('android.os.Build$VERSION') +SDK_INT = ANDROID_VERSION.SDK_INT + +try: + from android import config + ns = config.JAVA_NAMESPACE +except (ImportError, AttributeError): + ns = 'org.renpy.android' + +if 'PYTHON_SERVICE_ARGUMENT' in environ: + PythonService = autoclass(ns + '.PythonService') + activity = PythonService.mService +else: + PythonActivity = autoclass(ns + '.PythonActivity') + activity = PythonActivity.mActivity diff --git a/sbapp/plyer/platforms/android/accelerometer.py b/sbapp/plyer/platforms/android/accelerometer.py new file mode 100644 index 0000000..00c73cf --- /dev/null +++ b/sbapp/plyer/platforms/android/accelerometer.py @@ -0,0 +1,79 @@ +''' +Android accelerometer +--------------------- +''' + +from plyer.facades import Accelerometer +from jnius import PythonJavaClass, java_method, autoclass, cast +from plyer.platforms.android import activity + +Context = autoclass('android.content.Context') +Sensor = autoclass('android.hardware.Sensor') +SensorManager = autoclass('android.hardware.SensorManager') + + +class AccelerometerSensorListener(PythonJavaClass): + __javainterfaces__ = ['android/hardware/SensorEventListener'] + + def __init__(self): + super().__init__() + self.SensorManager = cast( + 'android.hardware.SensorManager', + activity.getSystemService(Context.SENSOR_SERVICE) + ) + self.sensor = self.SensorManager.getDefaultSensor( + Sensor.TYPE_ACCELEROMETER + ) + + self.values = [None, None, None] + + def enable(self): + self.SensorManager.registerListener( + self, self.sensor, + SensorManager.SENSOR_DELAY_NORMAL + ) + + def disable(self): + self.SensorManager.unregisterListener(self, self.sensor) + + @java_method('(Landroid/hardware/SensorEvent;)V') + def onSensorChanged(self, event): + self.values = event.values[:3] + + @java_method('(Landroid/hardware/Sensor;I)V') + def onAccuracyChanged(self, sensor, accuracy): + # Maybe, do something in future? + pass + + +class AndroidAccelerometer(Accelerometer): + def __init__(self): + super().__init__() + self.bState = False + + def _enable(self): + if (not self.bState): + self.listener = AccelerometerSensorListener() + self.listener.enable() + self.bState = True + + def _disable(self): + if (self.bState): + self.bState = False + self.listener.disable() + del self.listener + + def _get_acceleration(self): + if (self.bState): + return tuple(self.listener.values) + else: + return (None, None, None) + + def __del__(self): + if(self.bState): + self._disable() + super().__del__() + + +def instance(): + return AndroidAccelerometer() diff --git a/sbapp/plyer/platforms/android/audio.py b/sbapp/plyer/platforms/android/audio.py new file mode 100644 index 0000000..9f000ff --- /dev/null +++ b/sbapp/plyer/platforms/android/audio.py @@ -0,0 +1,58 @@ +from jnius import autoclass + +from plyer.facades.audio import Audio + +# Recorder Classes +MediaRecorder = autoclass('android.media.MediaRecorder') +AudioSource = autoclass('android.media.MediaRecorder$AudioSource') +OutputFormat = autoclass('android.media.MediaRecorder$OutputFormat') +AudioEncoder = autoclass('android.media.MediaRecorder$AudioEncoder') + +# Player Classes +MediaPlayer = autoclass('android.media.MediaPlayer') + + +class AndroidAudio(Audio): + '''Audio for android. + + For recording audio we use MediaRecorder Android class. + For playing audio we use MediaPlayer Android class. + ''' + + def __init__(self, file_path=None): + default_path = '/sdcard/testrecorder.3gp' + super().__init__(file_path or default_path) + + self._recorder = None + self._player = None + + def _start(self): + self._recorder = MediaRecorder() + self._recorder.setAudioSource(AudioSource.DEFAULT) + self._recorder.setOutputFormat(OutputFormat.DEFAULT) + self._recorder.setAudioEncoder(AudioEncoder.DEFAULT) + self._recorder.setOutputFile(self.file_path) + + self._recorder.prepare() + self._recorder.start() + + def _stop(self): + if self._recorder: + self._recorder.stop() + self._recorder.release() + self._recorder = None + + if self._player: + self._player.stop() + self._player.release() + self._player = None + + def _play(self): + self._player = MediaPlayer() + self._player.setDataSource(self.file_path) + self._player.prepare() + self._player.start() + + +def instance(): + return AndroidAudio() diff --git a/sbapp/plyer/platforms/android/barometer.py b/sbapp/plyer/platforms/android/barometer.py new file mode 100644 index 0000000..5b9ed48 --- /dev/null +++ b/sbapp/plyer/platforms/android/barometer.py @@ -0,0 +1,65 @@ +from jnius import autoclass +from jnius import cast +from jnius import java_method +from jnius import PythonJavaClass + +from plyer.facades import Barometer +from plyer.platforms.android import activity + +ActivityInfo = autoclass('android.content.pm.ActivityInfo') +Context = autoclass('android.content.Context') +Sensor = autoclass('android.hardware.Sensor') +SensorManager = autoclass('android.hardware.SensorManager') + + +class BarometerSensorListener(PythonJavaClass): + __javainterfaces__ = ['android/hardware/SensorEventListener'] + + def __init__(self): + super().__init__() + service = activity.getSystemService(Context.SENSOR_SERVICE) + self.SensorManager = cast('android.hardware.SensorManager', service) + + self.sensor = self.SensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE) + self.value = None + + def enable(self): + self.SensorManager.registerListener( + self, self.sensor, + SensorManager.SENSOR_DELAY_NORMAL + ) + + def disable(self): + self.SensorManager.unregisterListener(self, self.sensor) + + @java_method('(Landroid/hardware/SensorEvent;)V') + def onSensorChanged(self, event): + self.value = event.values[0] + + @java_method('(Landroid/hardware/Sensor;I)V') + def onAccuracyChanged(self, sensor, accuracy): + pass + + +class AndroidBarometer(Barometer): + + listener = None + + def _get_pressure(self): + if self.listener and self.listener.value: + pressure = self.listener.value + return pressure + + def _enable(self): + if not self.listener: + self.listener = BarometerSensorListener() + self.listener.enable() + + def _disable(self): + if self.listener: + self.listener.disable() + delattr(self, 'listener') + + +def instance(): + return AndroidBarometer() diff --git a/sbapp/plyer/platforms/android/battery.py b/sbapp/plyer/platforms/android/battery.py new file mode 100644 index 0000000..4a58f1f --- /dev/null +++ b/sbapp/plyer/platforms/android/battery.py @@ -0,0 +1,47 @@ +''' +Module of Android API for plyer.battery. +''' + +from jnius import autoclass, cast +from plyer.platforms.android import activity +from plyer.facades import Battery + +Intent = autoclass('android.content.Intent') +BatteryManager = autoclass('android.os.BatteryManager') +IntentFilter = autoclass('android.content.IntentFilter') + + +class AndroidBattery(Battery): + ''' + Implementation of Android battery API. + ''' + + def _get_state(self): + status = {"isCharging": None, "percentage": None} + + ifilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) + + battery_status = cast( + 'android.content.Intent', + activity.registerReceiver(None, ifilter) + ) + + query = battery_status.getIntExtra(BatteryManager.EXTRA_STATUS, -1) + is_charging = query == BatteryManager.BATTERY_STATUS_CHARGING + is_full = query == BatteryManager.BATTERY_STATUS_FULL + + level = battery_status.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + scale = battery_status.getIntExtra(BatteryManager.EXTRA_SCALE, -1) + percentage = (level / float(scale)) * 100 + + status['isCharging'] = is_charging or is_full + status['percentage'] = percentage + + return status + + +def instance(): + ''' + Instance for facade proxy. + ''' + return AndroidBattery() diff --git a/sbapp/plyer/platforms/android/bluetooth.py b/sbapp/plyer/platforms/android/bluetooth.py new file mode 100644 index 0000000..de72393 --- /dev/null +++ b/sbapp/plyer/platforms/android/bluetooth.py @@ -0,0 +1,32 @@ +''' +Module of Android API for plyer.bluetooth. +''' + +from jnius import autoclass +from plyer.platforms.android import activity +from plyer.facades import Bluetooth + +Global = autoclass('android.provider.Settings$Global') + + +class AndroidBluetooth(Bluetooth): + ''' + Implementation of Android Bluetooth API. + ''' + + def _get_info(self): + bluetooth_enabled = Global.getString( + activity.getContentResolver(), + Global.BLUETOOTH_ON + ) + status = 'off' + if bluetooth_enabled: + status = 'on' + return status + + +def instance(): + ''' + Instance for facade proxy. + ''' + return AndroidBluetooth() diff --git a/sbapp/plyer/platforms/android/brightness.py b/sbapp/plyer/platforms/android/brightness.py new file mode 100755 index 0000000..d0a1157 --- /dev/null +++ b/sbapp/plyer/platforms/android/brightness.py @@ -0,0 +1,34 @@ +''' +Android Brightness +------------------ +''' + +from jnius import autoclass +from plyer.facades import Brightness +from android import mActivity + +System = autoclass('android.provider.Settings$System') + + +class AndroidBrightness(Brightness): + + def _current_level(self): + + System.putInt( + mActivity.getContentResolver(), + System.SCREEN_BRIGHTNESS_MODE, + System.SCREEN_BRIGHTNESS_MODE_MANUAL) + cr_level = System.getInt( + mActivity.getContentResolver(), + System.SCREEN_BRIGHTNESS) + return (cr_level / 255.) * 100 + + def _set_level(self, level): + System.putInt( + mActivity.getContentResolver(), + System.SCREEN_BRIGHTNESS, + (level / 100.) * 255) + + +def instance(): + return AndroidBrightness() diff --git a/sbapp/plyer/platforms/android/call.py b/sbapp/plyer/platforms/android/call.py new file mode 100644 index 0000000..2a1388c --- /dev/null +++ b/sbapp/plyer/platforms/android/call.py @@ -0,0 +1,29 @@ +''' +Android Call +----------- +''' + +from jnius import autoclass +from plyer.facades import Call +from plyer.platforms.android import activity + +Intent = autoclass('android.content.Intent') +uri = autoclass('android.net.Uri') + + +class AndroidCall(Call): + + def _makecall(self, **kwargs): + + intent = Intent(Intent.ACTION_CALL) + tel = kwargs.get('tel') + intent.setData(uri.parse("tel:{}".format(tel))) + activity.startActivity(intent) + + def _dialcall(self, **kwargs): + intent_ = Intent(Intent.ACTION_DIAL) + activity.startActivity(intent_) + + +def instance(): + return AndroidCall() diff --git a/sbapp/plyer/platforms/android/camera.py b/sbapp/plyer/platforms/android/camera.py new file mode 100644 index 0000000..19707bd --- /dev/null +++ b/sbapp/plyer/platforms/android/camera.py @@ -0,0 +1,59 @@ +import android +import android.activity +from os import remove +from jnius import autoclass, cast +from plyer.facades import Camera +from plyer.platforms.android import activity + +Intent = autoclass('android.content.Intent') +PythonActivity = autoclass('org.kivy.android.PythonActivity') +MediaStore = autoclass('android.provider.MediaStore') +Uri = autoclass('android.net.Uri') + + +class AndroidCamera(Camera): + + def _take_picture(self, on_complete, filename=None): + assert(on_complete is not None) + self.on_complete = on_complete + self.filename = filename + android.activity.unbind(on_activity_result=self._on_activity_result) + android.activity.bind(on_activity_result=self._on_activity_result) + intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + uri = Uri.parse('file://' + filename) + parcelable = cast('android.os.Parcelable', uri) + intent.putExtra(MediaStore.EXTRA_OUTPUT, parcelable) + activity.startActivityForResult(intent, 0x123) + + def _take_video(self, on_complete, filename=None): + assert(on_complete is not None) + self.on_complete = on_complete + self.filename = filename + android.activity.unbind(on_activity_result=self._on_activity_result) + android.activity.bind(on_activity_result=self._on_activity_result) + intent = Intent(MediaStore.ACTION_VIDEO_CAPTURE) + uri = Uri.parse('file://' + filename) + parcelable = cast('android.os.Parcelable', uri) + intent.putExtra(MediaStore.EXTRA_OUTPUT, parcelable) + + # 0 = low quality, suitable for MMS messages, + # 1 = high quality + intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1) + activity.startActivityForResult(intent, 0x123) + + def _on_activity_result(self, requestCode, resultCode, intent): + if requestCode != 0x123: + return + android.activity.unbind(on_activity_result=self._on_activity_result) + if self.on_complete(self.filename): + self._remove(self.filename) + + def _remove(self, fn): + try: + remove(fn) + except OSError: + pass + + +def instance(): + return AndroidCamera() diff --git a/sbapp/plyer/platforms/android/compass.py b/sbapp/plyer/platforms/android/compass.py new file mode 100644 index 0000000..fcf0483 --- /dev/null +++ b/sbapp/plyer/platforms/android/compass.py @@ -0,0 +1,119 @@ +''' +Android Compass +--------------------- +''' + +from plyer.facades import Compass +from jnius import PythonJavaClass, java_method, autoclass, cast +from plyer.platforms.android import activity + +Context = autoclass('android.content.Context') +Sensor = autoclass('android.hardware.Sensor') +SensorManager = autoclass('android.hardware.SensorManager') + + +class MFUSensorListener(PythonJavaClass): + __javainterfaces__ = ['android/hardware/SensorEventListener'] + + def __init__(self): + super().__init__() + service = activity.getSystemService(Context.SENSOR_SERVICE) + self.SensorManager = cast('android.hardware.SensorManager', service) + + self.sensor = self.SensorManager.getDefaultSensor( + Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED) + self.values = [None, None, None, None, None, None] + + def enable(self): + self.SensorManager.registerListener( + self, self.sensor, + SensorManager.SENSOR_DELAY_NORMAL + ) + + def disable(self): + self.SensorManager.unregisterListener(self, self.sensor) + + @java_method('(Landroid/hardware/SensorEvent;)V') + def onSensorChanged(self, event): + self.values = event.values[:6] + + @java_method('(Landroid/hardware/Sensor;I)V') + def onAccuracyChanged(self, sensor, accuracy): + pass + + +class MagneticFieldSensorListener(PythonJavaClass): + __javainterfaces__ = ['android/hardware/SensorEventListener'] + + def __init__(self): + super().__init__() + self.SensorManager = cast( + 'android.hardware.SensorManager', + activity.getSystemService(Context.SENSOR_SERVICE) + ) + self.sensor = self.SensorManager.getDefaultSensor( + Sensor.TYPE_MAGNETIC_FIELD + ) + + self.values = [None, None, None] + + def enable(self): + self.SensorManager.registerListener( + self, self.sensor, + SensorManager.SENSOR_DELAY_NORMAL + ) + + def disable(self): + self.SensorManager.unregisterListener(self, self.sensor) + + @java_method('(Landroid/hardware/SensorEvent;)V') + def onSensorChanged(self, event): + self.values = event.values[:3] + + @java_method('(Landroid/hardware/Sensor;I)V') + def onAccuracyChanged(self, sensor, accuracy): + # Maybe, do something in future? + pass + + +class AndroidCompass(Compass): + def __init__(self): + super().__init__() + self.bState = False + + def _enable(self): + if (not self.bState): + self.listenerm = MagneticFieldSensorListener() + self.listenermu = MFUSensorListener() + self.listenerm.enable() + self.listenermu.enable() + self.bState = True + + def _disable(self): + if (self.bState): + self.bState = False + self.listenerm.disable() + self.listenermu.disable() + del self.listenerm + del self.listenermu + + def _get_orientation(self): + if (self.bState): + return tuple(self.listenerm.values) + else: + return (None, None, None) + + def _get_field_uncalib(self): + if (self.bState): + return tuple(self.listenermu.values) + else: + return (None, None, None, None, None, None) + + def __del__(self): + if(self.bState): + self._disable() + super().__del__() + + +def instance(): + return AndroidCompass() diff --git a/sbapp/plyer/platforms/android/devicename.py b/sbapp/plyer/platforms/android/devicename.py new file mode 100644 index 0000000..75855cc --- /dev/null +++ b/sbapp/plyer/platforms/android/devicename.py @@ -0,0 +1,33 @@ +''' +Module of Android API for plyer.devicename. +''' + +from jnius import autoclass +from plyer.facades import DeviceName + +Build = autoclass('android.os.Build') + + +class AndroidDeviceName(DeviceName): + ''' + Implementation of Android devicename API. + ''' + + def _get_device_name(self): + """ + Method to get the device name aka model in an android environment. + + Changed the implementation from 'android.provider.Settings.Global' to + 'android.os.Build' because 'android.provider.Settings.Global' was + introduced in API 17 whereas 'android.os.Build' is present since API 1 + + Thereby making this method more backward compatible. + """ + return Build.MODEL + + +def instance(): + ''' + Instance for facade proxy. + ''' + return AndroidDeviceName() diff --git a/sbapp/plyer/platforms/android/email.py b/sbapp/plyer/platforms/android/email.py new file mode 100644 index 0000000..95dfa5e --- /dev/null +++ b/sbapp/plyer/platforms/android/email.py @@ -0,0 +1,58 @@ +''' +Module of Android API for plyer.email. +''' + +from jnius import autoclass, cast +from plyer.facades import Email +from plyer.platforms.android import activity + +Intent = autoclass('android.content.Intent') +AndroidString = autoclass('java.lang.String') + + +class AndroidEmail(Email): + ''' + Implementation of Android email API. + ''' + + def _send(self, **kwargs): + intent = Intent(Intent.ACTION_SEND) + intent.setType('text/plain') + + recipient = kwargs.get('recipient') + subject = kwargs.get('subject') + text = kwargs.get('text') + create_chooser = kwargs.get('create_chooser') + + if recipient: + intent.putExtra(Intent.EXTRA_EMAIL, [recipient]) + if subject: + android_subject = cast( + 'java.lang.CharSequence', + AndroidString(subject) + ) + intent.putExtra(Intent.EXTRA_SUBJECT, android_subject) + if text: + android_text = cast( + 'java.lang.CharSequence', + AndroidString(text) + ) + intent.putExtra(Intent.EXTRA_TEXT, android_text) + + if create_chooser: + chooser_title = cast( + 'java.lang.CharSequence', + AndroidString('Send message with:') + ) + activity.startActivity( + Intent.createChooser(intent, chooser_title) + ) + else: + activity.startActivity(intent) + + +def instance(): + ''' + Instance for facade proxy. + ''' + return AndroidEmail() diff --git a/sbapp/plyer/platforms/android/filechooser.py b/sbapp/plyer/platforms/android/filechooser.py new file mode 100644 index 0000000..b809ccb --- /dev/null +++ b/sbapp/plyer/platforms/android/filechooser.py @@ -0,0 +1,447 @@ +''' +Android file chooser +-------------------- + +Android runs ``Activity`` asynchronously via pausing our ``PythonActivity`` +and starting a new one in the foreground. This means +``AndroidFileChooser._open_file()`` will always return the default value of +``AndroidFileChooser.selection`` i.e. ``None``. + +After the ``Activity`` (for us it's the file chooser ``Intent``) is completed, +Android moves it to the background (or destroys or whatever is implemented) +and pushes ``PythonActivity`` to the foreground. + +We have a custom listener for ``android.app.Activity.onActivityResult()`` +via `android` package from `python-for-android` recipe, +``AndroidFileChooser._on_activity_result()`` which is called independently of +any our action (we may call anything from our application in Python and this +handler will be called nevertheless on each ``android.app.Activity`` result +in the system). + +In the handler we check if the ``request_code`` matches the code passed to the +``Context.startActivityForResult()`` i.e. if the result from +``android.app.Activity`` is indeed meant for our ``PythonActivity`` and then we +proceed. + +Since the ``android.app.Activity.onActivityResult()`` is the only way for us +to intercept the result and we have a handler bound via ``android`` package, +we need to get the path/file/... selection to the user the same way. + +Threading + ``Thread.join()`` or ``time.sleep()`` or any other kind of waiting +for the result is not an option because: + +1) ``android.app.Activity.onActivityResult()`` might remain unexecuted if +the launched file chooser activity does not return the result (``Activity`` +dies/freezes/etc). + +2) Thread will be still waiting for the result e.g. an update of a value or +to actually finish, however the result from the call of +``AndroidFileChooser._open_file()`` will be returned nevertheless and anything +using that result will use an incorrect one i.e. the default value of +``AndroidFilechooser.selection`` (``None``). + +.. versionadded:: 1.4.0 +''' + +from os.path import join, basename +from random import randint + +from android import activity, mActivity +from jnius import autoclass, cast, JavaException +from plyer.facades import FileChooser +from plyer import storagepath + +Environment = autoclass("android.os.Environment") +String = autoclass('java.lang.String') +Intent = autoclass('android.content.Intent') +Activity = autoclass('android.app.Activity') +DocumentsContract = autoclass('android.provider.DocumentsContract') +ContentUris = autoclass('android.content.ContentUris') +Uri = autoclass('android.net.Uri') +Long = autoclass('java.lang.Long') +IMedia = autoclass('android.provider.MediaStore$Images$Media') +VMedia = autoclass('android.provider.MediaStore$Video$Media') +AMedia = autoclass('android.provider.MediaStore$Audio$Media') + + +class AndroidFileChooser(FileChooser): + ''' + FileChooser implementation for Android using + the built-in file browser via Intent. + + .. versionadded:: 1.4.0 + ''' + + # filechooser activity <-> result pair identification + select_code = None + + # default selection value + selection = None + + # select multiple files + multiple = False + + # mime types + mime_type = { + "doc": "application/msword", + "docx": "application/vnd.openxmlformats-officedocument." + + "wordprocessingml.document", + "ppt": "application/vnd.ms-powerpoint", + "pptx": "application/vnd.openxmlformats-officedocument." + + "presentationml.presentation", + "xls": "application/vnd.ms-excel", + "xlsx": "application/vnd.openxmlformats-officedocument." + + "spreadsheetml.sheet", + "text": "text/*", + "pdf": "application/pdf", + "zip": "application/zip", + "image": "image/*", + "video": "video/*", + "audio": "audio/*", + "application": "application/*"} + + selected_mime_type = None + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.select_code = randint(123456, 654321) + self.selection = None + + # bind a function for a response from filechooser activity + activity.bind(on_activity_result=self._on_activity_result) + + @staticmethod + def _handle_selection(selection): + ''' + Dummy placeholder for returning selection from + ``android.app.Activity.onActivityResult()``. + + .. versionadded:: 1.4.0 + ''' + return selection + + def _open_file(self, **kwargs): + ''' + Running Android Activity is non-blocking and the only call + that blocks is onActivityResult running in GUI thread + + .. versionadded:: 1.4.0 + ''' + + # set up selection handler + # startActivityForResult is async + # onActivityResult is sync + self._handle_selection = kwargs.pop( + 'on_selection', self._handle_selection + ) + self.selected_mime_type = \ + kwargs.pop("filters")[0] if "filters" in kwargs else "" + + # create Intent for opening + file_intent = Intent(Intent.ACTION_GET_CONTENT) + if not self.selected_mime_type or \ + type(self.selected_mime_type) != str or \ + self.selected_mime_type not in self.mime_type: + file_intent.setType("*/*") + else: + file_intent.setType(self.mime_type[self.selected_mime_type]) + file_intent.addCategory( + Intent.CATEGORY_OPENABLE + ) + + # use putExtra to allow multiple file selection + if kwargs.get('multiple', self.multiple): + file_intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, True) + + # start a new activity from PythonActivity + # which creates a filechooser via intent + mActivity.startActivityForResult( + Intent.createChooser(file_intent, cast( + 'java.lang.CharSequence', + String("FileChooser") + )), + self.select_code + ) + + def _on_activity_result(self, request_code, result_code, data): + ''' + Listener for ``android.app.Activity.onActivityResult()`` assigned + via ``android.activity.bind()``. + + .. versionadded:: 1.4.0 + ''' + + # not our response + if request_code != self.select_code: + return + + if result_code != Activity.RESULT_OK: + # The action had been cancelled. + return + + selection = [] + # Process multiple URI if multiple files selected + try: + for count in range(data.getClipData().getItemCount()): + ele = self._resolve_uri( + data.getClipData().getItemAt(count).getUri()) or [] + selection.append(ele) + except Exception: + selection = [self._resolve_uri(data.getData()), ] + + # return value to object + self.selection = selection + # return value via callback + self._handle_selection(selection) + + @staticmethod + def _handle_external_documents(uri): + ''' + Selection from the system filechooser when using ``Phone`` + or ``Internal storage`` or ``SD card`` option from menu. + + .. versionadded:: 1.4.0 + ''' + + file_id = DocumentsContract.getDocumentId(uri) + file_type, file_name = file_id.split(':') + + # internal SD card mostly mounted as a files storage in phone + internal = storagepath.get_external_storage_dir() + + # external (removable) SD card i.e. microSD + external = storagepath.get_sdcard_dir() + try: + external_base = basename(external) + except TypeError: + external_base = basename(internal) + + # resolve sdcard path + sd_card = internal + + # because external might have /storage/.../1 or other suffix + # and file_type might be only a part of the real folder in /storage + if file_type in external_base or external_base in file_type: + sd_card = external + elif file_type == "home": + sd_card = join(Environment.getExternalStorageDirectory( + ).getAbsolutePath(), Environment.DIRECTORY_DOCUMENTS) + + return join(sd_card, file_name) + + @staticmethod + def _handle_media_documents(uri): + ''' + Selection from the system filechooser when using ``Images`` + or ``Videos`` or ``Audio`` option from menu. + + .. versionadded:: 1.4.0 + ''' + + file_id = DocumentsContract.getDocumentId(uri) + file_type, file_name = file_id.split(':') + selection = '_id=?' + + if file_type == 'image': + uri = IMedia.EXTERNAL_CONTENT_URI + elif file_type == 'video': + uri = VMedia.EXTERNAL_CONTENT_URI + elif file_type == 'audio': + uri = AMedia.EXTERNAL_CONTENT_URI + return file_name, selection, uri + + @staticmethod + def _handle_downloads_documents(uri): + ''' + Selection from the system filechooser when using ``Downloads`` + option from menu. Might not work all the time due to: + + 1) invalid URI: + + jnius.jnius.JavaException: + JVM exception occurred: Unknown URI: + content://downloads/public_downloads/1034 + + 2) missing URI / android permissions + + jnius.jnius.JavaException: + JVM exception occurred: + Permission Denial: reading + com.android.providers.downloads.DownloadProvider uri + content://downloads/all_downloads/1034 from pid=2532, uid=10455 + requires android.permission.ACCESS_ALL_DOWNLOADS, + or grantUriPermission() + + Workaround: + Selecting path from ``Phone`` -> ``Download`` -> ```` + (or ``Internal storage``) manually. + + .. versionadded:: 1.4.0 + ''' + + # known locations, differ between machines + downloads = [ + 'content://downloads/public_downloads', + 'content://downloads/my_downloads', + + # all_downloads requires separate permission + # android.permission.ACCESS_ALL_DOWNLOADS + 'content://downloads/all_downloads' + ] + + file_id = DocumentsContract.getDocumentId(uri) + try_uris = [ + ContentUris.withAppendedId( + Uri.parse(down), Long.valueOf(file_id) + ) + for down in downloads + ] + + # try all known Download folder uris + # and handle JavaExceptions due to different locations + # for content:// downloads or missing permission + path = None + for down in try_uris: + try: + path = AndroidFileChooser._parse_content( + uri=down, projection=['_data'], + selection=None, + selection_args=None, + sort_order=None + ) + + except JavaException: + import traceback + traceback.print_exc() + + # we got a path, ignore the rest + if path: + break + + # alternative approach to Downloads by joining + # all data items from Activity result + if not path: + for down in try_uris: + try: + path = AndroidFileChooser._parse_content( + uri=down, projection=None, + selection=None, + selection_args=None, + sort_order=None, + index_all=True + ) + + except JavaException: + import traceback + traceback.print_exc() + + # we got a path, ignore the rest + if path: + break + return path + + def _resolve_uri(self, uri): + ''' + Resolve URI input from ``android.app.Activity.onActivityResult()``. + + .. versionadded:: 1.4.0 + ''' + + uri_authority = uri.getAuthority() + uri_scheme = uri.getScheme().lower() + + path = None + file_name = None + selection = None + downloads = None + + # This does not allow file selected from google photos or gallery + # or even any other file explorer to work + # not a document URI, nothing to convert from + # if not DocumentsContract.isDocumentUri(mActivity, uri): + # return path + + if uri_authority == 'com.android.externalstorage.documents': + return self._handle_external_documents(uri) + + # in case a user selects a file from 'Downloads' section + # note: this won't be triggered if a user selects a path directly + # e.g.: Phone -> Download -> + elif uri_authority == 'com.android.providers.downloads.documents': + path = downloads = self._handle_downloads_documents(uri) + + elif uri_authority == 'com.android.providers.media.documents': + file_name, selection, uri = self._handle_media_documents(uri) + + # parse content:// scheme to path + if uri_scheme == 'content' and not downloads: + try: + path = self._parse_content( + uri=uri, projection=['_data'], selection=selection, + selection_args=file_name, sort_order=None + ) + except JavaException: # handles array error for selection_args + path = self._parse_content( + uri=uri, projection=['_data'], selection=selection, + selection_args=[file_name], sort_order=None + ) + + # nothing to parse, file:// will return a proper path + elif uri_scheme == 'file': + path = uri.getPath() + + return path + + @staticmethod + def _parse_content( + uri, projection, selection, selection_args, sort_order, + index_all=False + ): + ''' + Parser for ``content://`` URI returned by some Android resources. + + .. versionadded:: 1.4.0 + ''' + + result = None + resolver = mActivity.getContentResolver() + read = Intent.FLAG_GRANT_READ_URI_PERMISSION + write = Intent.FLAG_GRANT_READ_URI_PERMISSION + persist = Intent.FLAG_GRANT_READ_URI_PERMISSION + + # grant permission for our activity + mActivity.grantUriPermission( + mActivity.getPackageName(), + uri, + read | write | persist + ) + + if not index_all: + cursor = resolver.query( + uri, projection, selection, + selection_args, sort_order + ) + + idx = cursor.getColumnIndex(projection[0]) + if idx != -1 and cursor.moveToFirst(): + result = cursor.getString(idx) + else: + result = [] + cursor = resolver.query( + uri, projection, selection, + selection_args, sort_order + ) + while cursor.moveToNext(): + for idx in range(cursor.getColumnCount()): + result.append(cursor.getString(idx)) + result = '/'.join(result) + return result + + def _file_selection_dialog(self, **kwargs): + mode = kwargs.pop('mode', None) + if mode == 'open': + self._open_file(**kwargs) + + +def instance(): + return AndroidFileChooser() diff --git a/sbapp/plyer/platforms/android/flash.py b/sbapp/plyer/platforms/android/flash.py new file mode 100644 index 0000000..eec1ae3 --- /dev/null +++ b/sbapp/plyer/platforms/android/flash.py @@ -0,0 +1,55 @@ +# coding=utf-8 +""" +Flash +----- +""" + +from plyer.facades import Flash +from jnius import autoclass +from plyer.platforms.android import activity + +Camera = autoclass("android.hardware.Camera") +CameraParameters = autoclass("android.hardware.Camera$Parameters") +SurfaceTexture = autoclass("android.graphics.SurfaceTexture") +PackageManager = autoclass('android.content.pm.PackageManager') +pm = activity.getPackageManager() +flash_available = pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH) + + +class AndroidFlash(Flash): + _camera = None + + def _on(self): + if self._camera is None: + self._camera_open() + if not self._camera: + return + self._camera.setParameters(self._f_on) + + def _off(self): + if not self._camera: + return + self._camera.setParameters(self._f_off) + + def _release(self): + if not self._camera: + return + self._camera.stopPreview() + self._camera.release() + self._camera = None + + def _camera_open(self): + if not flash_available: + return + self._camera = Camera.open() + self._f_on = Camera.getParameters() + self._f_off = Camera.getParameters() + self._f_on.setFlashMode(CameraParameters.FLASH_MODE_TORCH) + self._f_off.setFlashMode(CameraParameters.FLASH_MODE_OFF) + self._camera.startPreview() + # Need this for Nexus 5 + self._camera.setPreviewTexture(SurfaceTexture(0)) + + +def instance(): + return AndroidFlash() diff --git a/sbapp/plyer/platforms/android/gps.py b/sbapp/plyer/platforms/android/gps.py new file mode 100644 index 0000000..17fd86e --- /dev/null +++ b/sbapp/plyer/platforms/android/gps.py @@ -0,0 +1,82 @@ +''' +Android GPS +----------- +''' + +from plyer.facades import GPS +from plyer.platforms.android import activity +from jnius import autoclass, java_method, PythonJavaClass + +Looper = autoclass('android.os.Looper') +LocationManager = autoclass('android.location.LocationManager') +Context = autoclass('android.content.Context') + + +class _LocationListener(PythonJavaClass): + __javainterfaces__ = ['android/location/LocationListener'] + + def __init__(self, root): + self.root = root + super().__init__() + + @java_method('(Landroid/location/Location;)V') + def onLocationChanged(self, location): + self.root.on_location( + lat=location.getLatitude(), + lon=location.getLongitude(), + speed=location.getSpeed(), + bearing=location.getBearing(), + altitude=location.getAltitude(), + accuracy=location.getAccuracy()) + + @java_method('(Ljava/lang/String;)V') + def onProviderEnabled(self, status): + if self.root.on_status: + self.root.on_status('provider-enabled', status) + + @java_method('(Ljava/lang/String;)V') + def onProviderDisabled(self, status): + if self.root.on_status: + self.root.on_status('provider-disabled', status) + + @java_method('(Ljava/lang/String;ILandroid/os/Bundle;)V') + def onStatusChanged(self, provider, status, extras): + if self.root.on_status: + s_status = 'unknown' + if status == 0x00: + s_status = 'out-of-service' + elif status == 0x01: + s_status = 'temporarily-unavailable' + elif status == 0x02: + s_status = 'available' + self.root.on_status('provider-status', '{}: {}'.format( + provider, s_status)) + + +class AndroidGPS(GPS): + + def _configure(self): + if not hasattr(self, '_location_manager'): + self._location_manager = activity.getSystemService( + Context.LOCATION_SERVICE + ) + self._location_listener = _LocationListener(self) + + def _start(self, **kwargs): + min_time = kwargs.get('minTime') + min_distance = kwargs.get('minDistance') + providers = self._location_manager.getProviders(False).toArray() + for provider in providers: + self._location_manager.requestLocationUpdates( + provider, + min_time, # minTime, in milliseconds + min_distance, # minDistance, in meters + self._location_listener, + Looper.getMainLooper()) + + def _stop(self): + self._location_manager.removeUpdates(self._location_listener) + + +def instance(): + return AndroidGPS() diff --git a/sbapp/plyer/platforms/android/gravity.py b/sbapp/plyer/platforms/android/gravity.py new file mode 100644 index 0000000..9cb4100 --- /dev/null +++ b/sbapp/plyer/platforms/android/gravity.py @@ -0,0 +1,84 @@ +''' +Android gravity +--------------------- +''' + +from jnius import autoclass +from jnius import cast +from jnius import java_method +from jnius import PythonJavaClass + +from plyer.facades import Gravity +from plyer.platforms.android import activity + +Context = autoclass('android.content.Context') +Sensor = autoclass('android.hardware.Sensor') +SensorManager = autoclass('android.hardware.SensorManager') + + +class GravitySensorListener(PythonJavaClass): + __javainterfaces__ = ['android/hardware/SensorEventListener'] + + def __init__(self): + super().__init__() + + service = activity.getSystemService(Context.SENSOR_SERVICE) + self.SensorManager = cast('android.hardware.SensorManager', service) + + self.sensor = self.SensorManager.getDefaultSensor( + Sensor.TYPE_GRAVITY + ) + + self.values = [None, None, None] + + def enable(self): + self.SensorManager.registerListener( + self, + self.sensor, + SensorManager.SENSOR_DELAY_NORMAL + ) + + def disable(self): + self.SensorManager.unregisterListener(self, self.sensor) + + @java_method('(Landroid/hardware/SensorEvent;)V') + def onSensorChanged(self, event): + self.values = event.values[:3] + + @java_method('(Landroid/hardware/Sensor;I)V') + def onAccuracyChanged(self, sensor, accuracy): + pass + + +class AndroidGravity(Gravity): + + def __init__(self): + super().__init__() + self.state = False + + def _enable(self): + if not self.state: + self.listener = GravitySensorListener() + self.listener.enable() + self.state = True + + def _disable(self): + if self.state: + self.state = False + self.listener.disable() + del self.listener + + def _get_gravity(self): + if self.state: + return tuple(self.listener.values) + else: + return (None, None, None) + + def __del__(self): + if self.state: + self._disable() + super().__del__() + + +def instance(): + return AndroidGravity() diff --git a/sbapp/plyer/platforms/android/gyroscope.py b/sbapp/plyer/platforms/android/gyroscope.py new file mode 100644 index 0000000..d6382b1 --- /dev/null +++ b/sbapp/plyer/platforms/android/gyroscope.py @@ -0,0 +1,119 @@ +''' +Android Gyroscope +----------------- +''' + +from plyer.facades import Gyroscope +from jnius import PythonJavaClass, java_method, autoclass, cast +from plyer.platforms.android import activity + +Context = autoclass('android.content.Context') +Sensor = autoclass('android.hardware.Sensor') +SensorManager = autoclass('android.hardware.SensorManager') + + +class GyroscopeSensorListener(PythonJavaClass): + __javainterfaces__ = ['android/hardware/SensorEventListener'] + + def __init__(self): + super().__init__() + self.SensorManager = cast( + 'android.hardware.SensorManager', + activity.getSystemService(Context.SENSOR_SERVICE) + ) + self.sensor = self.SensorManager.getDefaultSensor( + Sensor.TYPE_GYROSCOPE + ) + + self.values = [None, None, None] + + def enable(self): + self.SensorManager.registerListener( + self, self.sensor, + SensorManager.SENSOR_DELAY_NORMAL + ) + + def disable(self): + self.SensorManager.unregisterListener(self, self.sensor) + + @java_method('(Landroid/hardware/SensorEvent;)V') + def onSensorChanged(self, event): + self.values = event.values[:3] + + @java_method('(Landroid/hardware/Sensor;I)V') + def onAccuracyChanged(self, sensor, accuracy): + # Maybe, do something in future? + pass + + +class GyroUncalibratedSensorListener(PythonJavaClass): + __javainterfaces__ = ['android/hardware/SensorEventListener'] + + def __init__(self): + super().__init__() + service = activity.getSystemService(Context.SENSOR_SERVICE) + self.SensorManager = cast('android.hardware.SensorManager', service) + + self.sensor = self.SensorManager.getDefaultSensor( + Sensor.TYPE_GYROSCOPE_UNCALIBRATED) + self.values = [None, None, None, None, None, None] + + def enable(self): + self.SensorManager.registerListener( + self, self.sensor, + SensorManager.SENSOR_DELAY_NORMAL + ) + + def disable(self): + self.SensorManager.unregisterListener(self, self.sensor) + + @java_method('(Landroid/hardware/SensorEvent;)V') + def onSensorChanged(self, event): + self.values = event.values[:6] + + @java_method('(Landroid/hardware/Sensor;I)V') + def onAccuracyChanged(self, sensor, accuracy): + pass + + +class AndroidGyroscope(Gyroscope): + def __init__(self): + super().__init__() + self.bState = False + + def _enable(self): + if (not self.bState): + self.listenerg = GyroscopeSensorListener() + self.listenergu = GyroUncalibratedSensorListener() + self.listenerg.enable() + self.listenergu.enable() + self.bState = True + + def _disable(self): + if (self.bState): + self.bState = False + self.listenerg.disable() + self.listenergu.disable() + del self.listenerg + del self.listenergu + + def _get_orientation(self): + if (self.bState): + return tuple(self.listenerg.values) + else: + return (None, None, None) + + def _get_rotation_uncalib(self): + if (self.bState): + return tuple(self.listenergu.values) + else: + return (None, None, None, None, None, None) + + def __del__(self): + if(self.bState): + self._disable() + super().__del__() + + +def instance(): + return AndroidGyroscope() diff --git a/sbapp/plyer/platforms/android/humidity.py b/sbapp/plyer/platforms/android/humidity.py new file mode 100644 index 0000000..78e17ac --- /dev/null +++ b/sbapp/plyer/platforms/android/humidity.py @@ -0,0 +1,107 @@ +from jnius import autoclass +from jnius import cast +from jnius import java_method +from jnius import PythonJavaClass +from math import exp +from plyer.facades import Humidity +from plyer.platforms.android import activity + +ActivityInfo = autoclass('android.content.pm.ActivityInfo') +Context = autoclass('android.content.Context') +Sensor = autoclass('android.hardware.Sensor') +SensorManager = autoclass('android.hardware.SensorManager') + + +class RelativeHumiditySensorListener(PythonJavaClass): + __javainterfaces__ = ['android/hardware/SensorEventListener'] + + def __init__(self): + super().__init__() + service = activity.getSystemService(Context.SENSOR_SERVICE) + self.SensorManager = cast('android.hardware.SensorManager', service) + + self.sensor = self.SensorManager.getDefaultSensor( + Sensor.TYPE_RELATIVE_HUMIDITY) + self.value = None + + def enable(self): + self.SensorManager.registerListener(self, self.sensor, + SensorManager.SENSOR_DELAY_NORMAL) + + def disable(self): + self.SensorManager.unregisterListener(self, self.sensor) + + @java_method('(Landroid/hardware/SensorEvent;)V') + def onSensorChanged(self, event): + self.value = event.values[0] + + @java_method('(Landroid/hardware/Sensor;I)V') + def onAccuracyChanged(self, sensor, accuracy): + pass + + +class AmbientTemperatureSensorListener(PythonJavaClass): + __javainterfaces__ = ['android/hardware/SensorEventListener'] + + def __init__(self): + super().__init__() + service = activity.getSystemService(Context.SENSOR_SERVICE) + self.SensorManager = cast('android.hardware.SensorManager', service) + + self.sensor = self.SensorManager.getDefaultSensor( + Sensor.TYPE_AMBIENT_TEMPERATURE) + self.value = None + + def enable(self): + self.SensorManager.registerListener(self, self.sensor, + SensorManager.SENSOR_DELAY_NORMAL) + + def disable(self): + self.SensorManager.unregisterListener(self, self.sensor) + + @java_method('(Landroid/hardware/SensorEvent;)V') + def onSensorChanged(self, event): + self.value = event.values[0] + + @java_method('(Landroid/hardware/Sensor;I)V') + def onAccuracyChanged(self, sensor, accuracy): + pass + + +class AndroidHumidity(Humidity): + + def __init__(self): + self.state = False + + def _get_humidity(self): + if self.state: + m = 17.62 + Tn = 243.12 + Ta = 216.7 + Rh = self.listener_r.value + Tc = self.listener_a.value + A = 6.112 + K = 273.15 + humidity = (Ta * (Rh / 100) * A * exp(m * Tc / (Tn + Tc)) + / (K + Tc)) + return humidity + + def _enable(self): + if not self.state: + self.listener_r = RelativeHumiditySensorListener() + self.listener_a = AmbientTemperatureSensorListener() + self.listener_r.enable() + self.listener_a.enable() + self.state = True + + def _disable(self): + if self.state: + self.listener_r.disable() + self.listener_a.disable() + self.state = False + delattr(self, 'listener_r') + delattr(self, 'listener_a') + + +def instance(): + return AndroidHumidity() diff --git a/sbapp/plyer/platforms/android/irblaster.py b/sbapp/plyer/platforms/android/irblaster.py new file mode 100644 index 0000000..06d311b --- /dev/null +++ b/sbapp/plyer/platforms/android/irblaster.py @@ -0,0 +1,55 @@ +from jnius import autoclass + +from plyer.facades import IrBlaster +from plyer.platforms.android import activity, SDK_INT, ANDROID_VERSION + +if SDK_INT >= 19: + Context = autoclass('android.content.Context') + ir_manager = activity.getSystemService(Context.CONSUMER_IR_SERVICE) +else: + ir_manager = None + + +class AndroidIrBlaster(IrBlaster): + def _exists(self): + if ir_manager and ir_manager.hasIrEmitter(): + return True + return False + + @property + def multiply_pulse(self): + '''Android 4.4.3+ uses microseconds instead of period counts + ''' + return not (SDK_INT == 19 + and int(str(ANDROID_VERSION.RELEASE).rsplit('.', 1)[-1]) + < 3) + + def _get_frequencies(self): + if not ir_manager: + return None + + if hasattr(self, '_frequencies'): + return self._frequencies + + ir_frequencies = ir_manager.getCarrierFrequencies() + if not ir_frequencies: + return [] + + frequencies = [] + for freqrange in ir_frequencies: + freq = (freqrange.getMinFrequency(), freqrange.getMaxFrequency()) + frequencies.append(freq) + + self._frequencies = frequencies + return frequencies + + def _transmit(self, frequency, pattern, mode): + if self.multiply_pulse and mode == 'period': + pattern = self.periods_to_microseconds(frequency, pattern) + elif not self.multiply_pulse and mode == 'microseconds': + pattern = self.microseconds_to_periods(frequency, pattern) + ir_manager.transmit(frequency, pattern) + + +def instance(): + return AndroidIrBlaster() diff --git a/sbapp/plyer/platforms/android/keystore.py b/sbapp/plyer/platforms/android/keystore.py new file mode 100644 index 0000000..0339948 --- /dev/null +++ b/sbapp/plyer/platforms/android/keystore.py @@ -0,0 +1,25 @@ +from plyer.facades import Keystore +from plyer.platforms.android import activity + + +class AndroidKeystore(Keystore): + + def _set_key(self, servicename, key, value, **kwargs): + mode = kwargs.get("mode", 0) + settings = activity.getSharedPreferences(servicename, mode) + editor = settings.edit() + editor.putString(key, value) + editor.commit() + + def _get_key(self, servicename, key, **kwargs): + mode = kwargs.get("mode", 0) + default = kwargs.get("default", "__None") + settings = activity.getSharedPreferences(servicename, mode) + ret = settings.getString(key, default) + if ret == "__None": + ret = None + return ret + + +def instance(): + return AndroidKeystore() diff --git a/sbapp/plyer/platforms/android/light.py b/sbapp/plyer/platforms/android/light.py new file mode 100644 index 0000000..13dd394 --- /dev/null +++ b/sbapp/plyer/platforms/android/light.py @@ -0,0 +1,63 @@ +from jnius import autoclass +from jnius import cast +from jnius import java_method +from jnius import PythonJavaClass + +from plyer.facades import Light +from plyer.platforms.android import activity + +Context = autoclass('android.content.Context') +Sensor = autoclass('android.hardware.Sensor') +SensorManager = autoclass('android.hardware.SensorManager') + + +class LightSensorListener(PythonJavaClass): + __javainterfaces__ = ['android/hardware/SensorEventListener'] + + def __init__(self): + super().__init__() + service = activity.getSystemService(Context.SENSOR_SERVICE) + self.SensorManager = cast('android.hardware.SensorManager', service) + self.sensor = self.SensorManager.getDefaultSensor(Sensor.TYPE_LIGHT) + self.value = None + + def enable(self): + self.SensorManager.registerListener( + self, self.sensor, + SensorManager.SENSOR_DELAY_NORMAL + ) + + def disable(self): + self.SensorManager.unregisterListener(self, self.sensor) + + @java_method('(Landroid/hardware/SensorEvent;)V') + def onSensorChanged(self, event): + self.value = event.values[0] + + @java_method('(Landroid/hardware/Sensor;I)V') + def onAccuracyChanged(self, sensor, accuracy): + pass + + +class AndroidLight(Light): + + listener = None + + def _get_illumination(self): + if self.listener and self.listener.value: + light = self.listener.value + return light + + def _enable(self): + if not self.listener: + self.listener = LightSensorListener() + self.listener.enable() + + def _disable(self): + if self.listener: + self.listener.disable() + delattr(self, 'listener') + + +def instance(): + return AndroidLight() diff --git a/sbapp/plyer/platforms/android/notification.py b/sbapp/plyer/platforms/android/notification.py new file mode 100644 index 0000000..24a567d --- /dev/null +++ b/sbapp/plyer/platforms/android/notification.py @@ -0,0 +1,208 @@ +''' +Module of Android API for plyer.notification. + +.. versionadded:: 1.0.0 + +.. versionchanged:: 1.4.0 + Fixed notifications not displaying due to missing NotificationChannel + required by Android Oreo 8.0+ (API 26+). + +.. versionchanged:: 1.4.0 + Added simple toaster notification. + +.. versionchanged:: 1.4.0 + Fixed notifications not displaying big icons properly. + Added option for custom big icon via `icon`. +''' + +from android import python_act +from android.runnable import run_on_ui_thread +from jnius import autoclass, cast + +from plyer.facades import Notification +from plyer.platforms.android import activity, SDK_INT + +AndroidString = autoclass('java.lang.String') +Context = autoclass('android.content.Context') +NotificationBuilder = autoclass('android.app.Notification$Builder') +NotificationManager = autoclass('android.app.NotificationManager') +PendingIntent = autoclass('android.app.PendingIntent') +Intent = autoclass('android.content.Intent') +Toast = autoclass('android.widget.Toast') +BitmapFactory = autoclass('android.graphics.BitmapFactory') +Icon = autoclass("android.graphics.drawable.Icon") + + +class AndroidNotification(Notification): + ''' + Implementation of Android notification API. + + .. versionadded:: 1.0.0 + ''' + + def __init__(self): + package_name = activity.getPackageName() + self._ns = None + self._channel_id = package_name + + pm = activity.getPackageManager() + info = pm.getActivityInfo(activity.getComponentName(), 0) + if info.icon == 0: + # Take the application icon instead. + info = pm.getApplicationInfo(package_name, 0) + + self._app_icon = info.icon + + def _get_notification_service(self): + if not self._ns: + self._ns = cast(NotificationManager, activity.getSystemService( + Context.NOTIFICATION_SERVICE + )) + return self._ns + + def _build_notification_channel(self, name): + ''' + Create a NotificationChannel using channel id of the application + package name (com.xyz, org.xyz, ...) and channel name same as the + provided notification title if the API is high enough, otherwise + do nothing. + + .. versionadded:: 1.4.0 + ''' + + if SDK_INT < 26: + return + + channel = autoclass('android.app.NotificationChannel') + + app_channel = channel( + self._channel_id, name, NotificationManager.IMPORTANCE_DEFAULT + ) + self._get_notification_service().createNotificationChannel( + app_channel + ) + return app_channel + + @run_on_ui_thread + def _toast(self, message): + ''' + Display a popup-like small notification at the bottom of the screen. + + .. versionadded:: 1.4.0 + ''' + Toast.makeText( + activity, + cast('java.lang.CharSequence', AndroidString(message)), + Toast.LENGTH_LONG + ).show() + + def _set_icons(self, notification, icon=None, notification_icon=None): + ''' + Set the small application icon displayed at the top panel together with + WiFi, battery percentage and time and the big optional icon (preferably + PNG format with transparent parts) displayed directly in the + notification body. + + .. versionadded:: 1.4.0 + ''' + if notification_icon == None: + app_icon = self._app_icon + else: + notification_icon_bitmap = BitmapFactory.decodeFile(notification_icon) + app_icon = Icon.createWithBitmap(notification_icon_bitmap) + + notification.setSmallIcon(app_icon) + + bitmap_icon = app_icon + if icon is not None: + bitmap_icon = BitmapFactory.decodeFile(icon) + notification.setLargeIcon(bitmap_icon) + elif icon == '': + # we don't want the big icon set, + # only the small one in the top panel + pass + else: + bitmap_icon = BitmapFactory.decodeResource( + python_act.getResources(), app_icon + ) + notification.setLargeIcon(bitmap_icon) + + def _build_notification(self, title): + ''' + .. versionadded:: 1.4.0 + ''' + if SDK_INT < 26: + noti = NotificationBuilder(activity) + else: + self._channel = self._build_notification_channel(title) + noti = NotificationBuilder(activity, self._channel_id) + return noti + + @staticmethod + def _set_open_behavior(notification): + ''' + Open the source application when user opens the notification. + + .. versionadded:: 1.4.0 + ''' + + # create Intent that navigates back to the application + app_context = activity.getApplication().getApplicationContext() + notification_intent = Intent(app_context, python_act) + + # set flags to run our application Activity + notification_intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + notification_intent.setAction(Intent.ACTION_MAIN) + notification_intent.addCategory(Intent.CATEGORY_LAUNCHER) + + # get our application Activity + pending_intent = PendingIntent.getActivity( + app_context, 0, notification_intent, 0 + ) + + notification.setContentIntent(pending_intent) + notification.setAutoCancel(True) + + def _open_notification(self, notification): + if SDK_INT >= 16: + notification = notification.build() + else: + notification = notification.getNotification() + + self._get_notification_service().notify(0, notification) + + def _notify(self, **kwargs): + noti = None + message = kwargs.get('message').encode('utf-8') + ticker = kwargs.get('ticker').encode('utf-8') + title = AndroidString( + kwargs.get('title', '').encode('utf-8') + ) + icon = kwargs.get('app_icon') + notification_icon = kwargs.get('notification_icon') + + # decide whether toast only or proper notification + if kwargs.get('toast'): + self._toast(message) + return + else: + noti = self._build_notification(title) + + # set basic properties for notification + noti.setContentTitle(title) + noti.setContentText(AndroidString(message)) + noti.setTicker(AndroidString(ticker)) + + # set additional flags for notification + self._set_icons(noti, icon=icon, notification_icon=notification_icon) + self._set_open_behavior(noti) + + # launch + self._open_notification(noti) + + +def instance(): + ''' + Instance for facade proxy. + ''' + return AndroidNotification() diff --git a/sbapp/plyer/platforms/android/orientation.py b/sbapp/plyer/platforms/android/orientation.py new file mode 100644 index 0000000..5f10958 --- /dev/null +++ b/sbapp/plyer/platforms/android/orientation.py @@ -0,0 +1,43 @@ +from jnius import autoclass +from plyer.platforms.android import activity +from plyer.facades import Orientation + +ActivityInfo = autoclass('android.content.pm.ActivityInfo') + + +class AndroidOrientation(Orientation): + + def _set_landscape(self, **kwargs): + reverse = kwargs.get('reverse') + if reverse: + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) + else: + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) + + def _set_portrait(self, **kwargs): + reverse = kwargs.get('reverse') + if reverse: + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT) + else: + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) + + def _set_sensor(self, **kwargs): + mode = kwargs.get('mode') + + if mode == 'any': + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_SENSOR) + elif mode == 'landscape': + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) + elif mode == 'portrait': + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) + + +def instance(): + return AndroidOrientation() diff --git a/sbapp/plyer/platforms/android/proximity.py b/sbapp/plyer/platforms/android/proximity.py new file mode 100644 index 0000000..b33ecda --- /dev/null +++ b/sbapp/plyer/platforms/android/proximity.py @@ -0,0 +1,69 @@ +from jnius import autoclass +from jnius import cast +from jnius import java_method +from jnius import PythonJavaClass + +from plyer.platforms.android import activity +from plyer.facades import Proximity + +ActivityInfo = autoclass('android.content.pm.ActivityInfo') +Context = autoclass('android.content.Context') +Sensor = autoclass('android.hardware.Sensor') +SensorManager = autoclass('android.hardware.SensorManager') + + +class ProximitySensorListener(PythonJavaClass): + __javainterfaces__ = ['android/hardware/SensorEventListener'] + + def __init__(self): + super().__init__() + service = activity.getSystemService(Context.SENSOR_SERVICE) + self.SensorManager = cast('android.hardware.SensorManager', service) + + self.sensor = self.SensorManager.getDefaultSensor( + Sensor.TYPE_PROXIMITY) + self.value = None + + def enable(self): + self.SensorManager.registerListener( + self, self.sensor, + SensorManager.SENSOR_DELAY_NORMAL + ) + + def disable(self): + self.SensorManager.unregisterListener(self, self.sensor) + + @java_method('(Landroid/hardware/SensorEvent;)V') + def onSensorChanged(self, event): + self.value = event.values[0] + + @java_method('(Landroid/hardware/Sensor;I)V') + def onAccuracyChanged(self, sensor, accuracy): + pass + + +class AndroidProximity(Proximity): + + listener = None + + def _enable(self, **kwargs): + if not self.listener: + self.listener = ProximitySensorListener() + self.listener.enable() + + def _disable(self, **kwargs): + if self.listener: + self.listener.disable() + delattr(self, 'listener') + + def _get_proximity(self): + if self.listener: + value = self.listener.value + # value is 0.0 when proxime sensor is covered. In other case + # value is 5.0 because in smartphone, optical proximity sensors + # are used. + return value < 5.0 + + +def instance(): + return AndroidProximity() diff --git a/sbapp/plyer/platforms/android/sms.py b/sbapp/plyer/platforms/android/sms.py new file mode 100644 index 0000000..8650968 --- /dev/null +++ b/sbapp/plyer/platforms/android/sms.py @@ -0,0 +1,25 @@ +''' +Android SMS +----------- +''' + +from jnius import autoclass +from plyer.facades import Sms + +SmsManager = autoclass('android.telephony.SmsManager') + + +class AndroidSms(Sms): + + def _send(self, **kwargs): + sms = SmsManager.getDefault() + + recipient = kwargs.get('recipient') + message = kwargs.get('message') + + if sms: + sms.sendTextMessage(recipient, None, message, None, None) + + +def instance(): + return AndroidSms() diff --git a/sbapp/plyer/platforms/android/spatialorientation.py b/sbapp/plyer/platforms/android/spatialorientation.py new file mode 100644 index 0000000..e0df1d3 --- /dev/null +++ b/sbapp/plyer/platforms/android/spatialorientation.py @@ -0,0 +1,118 @@ +from jnius import autoclass +from jnius import cast +from jnius import java_method +from jnius import PythonJavaClass +from plyer.platforms.android import activity +from plyer.facades import SpatialOrientation + +Context = autoclass('android.content.Context') +Sensor = autoclass('android.hardware.Sensor') +SensorManager = autoclass('android.hardware.SensorManager') + + +class AccelerometerSensorListener(PythonJavaClass): + __javainterfaces__ = ['android/hardware/SensorEventListener'] + + def __init__(self): + super().__init__() + self.SensorManager = cast( + 'android.hardware.SensorManager', + activity.getSystemService(Context.SENSOR_SERVICE) + ) + self.sensor = self.SensorManager.getDefaultSensor( + Sensor.TYPE_ACCELEROMETER + ) + self.values = [None, None, None] + + def enable(self): + self.SensorManager.registerListener( + self, self.sensor, + SensorManager.SENSOR_DELAY_NORMAL + ) + + def disable(self): + self.SensorManager.unregisterListener(self, self.sensor) + + @java_method('(Landroid/hardware/SensorEvent;)V') + def onSensorChanged(self, event): + self.values = event.values[:3] + + @java_method('(Landroid/hardware/Sensor;I)V') + def onAccuracyChanged(self, sensor, accuracy): + pass + + +class MagnetometerSensorListener(PythonJavaClass): + __javainterfaces__ = ['android/hardware/SensorEventListener'] + + def __init__(self): + super().__init__() + service = activity.getSystemService(Context.SENSOR_SERVICE) + self.SensorManager = cast('android.hardware.SensorManager', service) + + self.sensor = self.SensorManager.getDefaultSensor( + Sensor.TYPE_MAGNETIC_FIELD) + self.values = [None, None, None] + + def enable(self): + self.SensorManager.registerListener( + self, self.sensor, + SensorManager.SENSOR_DELAY_NORMAL + ) + + def disable(self): + self.SensorManager.unregisterListener(self, self.sensor) + + @java_method('(Landroid/hardware/SensorEvent;)V') + def onSensorChanged(self, event): + self.values = event.values[:3] + + @java_method('(Landroid/hardware/Sensor;I)V') + def onAccuracyChanged(self, sensor, accuracy): + pass + + +class AndroidSpOrientation(SpatialOrientation): + + def __init__(self): + self.state = False + + def _get_orientation(self): + if self.state: + rotation = [0] * 9 + inclination = [0] * 9 + gravity = [] + geomagnetic = [] + gravity = self.listener_a.values + geomagnetic = self.listener_m.values + if gravity[0] is not None and geomagnetic[0] is not None: + ff_state = SensorManager.getRotationMatrix( + rotation, inclination, + gravity, geomagnetic + ) + if ff_state: + values = [0, 0, 0] + values = SensorManager.getOrientation( + rotation, values + ) + return values + + def _enable_listener(self, **kwargs): + if not self.state: + self.listener_a = AccelerometerSensorListener() + self.listener_m = MagnetometerSensorListener() + self.listener_a.enable() + self.listener_m.enable() + self.state = True + + def _disable_listener(self, **kwargs): + if self.state: + self.listener_a.disable() + self.listener_m.disable() + self.state = False + delattr(self, 'listener_a') + delattr(self, 'listener_m') + + +def instance(): + return AndroidSpOrientation() diff --git a/sbapp/plyer/platforms/android/storagepath.py b/sbapp/plyer/platforms/android/storagepath.py new file mode 100755 index 0000000..788e3fc --- /dev/null +++ b/sbapp/plyer/platforms/android/storagepath.py @@ -0,0 +1,69 @@ +''' +Android Storage Path +-------------------- +''' + +from os import listdir, access, R_OK +from os.path import join +from plyer.facades import StoragePath +from jnius import autoclass +from android import mActivity + +Environment = autoclass('android.os.Environment') +Context = autoclass('android.content.Context') + + +class AndroidStoragePath(StoragePath): + + def _get_home_dir(self): + return Environment.getDataDirectory().getAbsolutePath() + + def _get_external_storage_dir(self): + return Environment.getExternalStorageDirectory().getAbsolutePath() + + def _get_sdcard_dir(self): + ''' + .. versionadded:: 1.4.0 + ''' + # folder in /storage/ that is readable + # and is not internal SD card + path = None + for folder in listdir('/storage'): + folder = join('/storage', folder) + if folder in self._get_external_storage_dir(): + continue + if not access(folder, R_OK): + continue + path = folder + break + return path + + def _get_root_dir(self): + return Environment.getRootDirectory().getAbsolutePath() + + def _get_documents_dir(self): + return Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOCUMENTS).getAbsolutePath() + + def _get_downloads_dir(self): + return Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() + + def _get_videos_dir(self): + return Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_MOVIES).getAbsolutePath() + + def _get_music_dir(self): + return Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_MUSIC).getAbsolutePath() + + def _get_pictures_dir(self): + return Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_PICTURES).getAbsolutePath() + + def _get_application_dir(self): + return mActivity.getFilesDir().getParentFile().getParent() + + +def instance(): + return AndroidStoragePath() diff --git a/sbapp/plyer/platforms/android/stt.py b/sbapp/plyer/platforms/android/stt.py new file mode 100644 index 0000000..51681e0 --- /dev/null +++ b/sbapp/plyer/platforms/android/stt.py @@ -0,0 +1,252 @@ +from android.runnable import run_on_ui_thread + +from jnius import autoclass +from jnius import java_method +from jnius import PythonJavaClass + +from plyer.facades import STT +from plyer.platforms.android import activity + +ArrayList = autoclass('java.util.ArrayList') +Bundle = autoclass('android.os.Bundle') +Context = autoclass('android.content.Context') +Intent = autoclass('android.content.Intent') +RecognizerIntent = autoclass('android.speech.RecognizerIntent') +RecognitionListener = autoclass('android.speech.RecognitionListener') +SpeechRecognizer = autoclass('android.speech.SpeechRecognizer') + +SpeechResults = SpeechRecognizer.RESULTS_RECOGNITION + + +class SpeechListener(PythonJavaClass): + __javainterfaces__ = ['android/speech/RecognitionListener'] + + # class variables because PythonJavaClass class failed + # to see them later in getters and setters + _error_callback = None + _result_callback = None + _partial_result_callback = None + _volume_callback = None + + def __init__(self): + super().__init__() + + # overwrite class variables in the object + self._error_callback = None + self._result_callback = None + self._partial_result_callback = None + self._volume_callback = None + + # error handling + @property + def error_callback(self): + return self._error_callback + + @error_callback.setter + def error_callback(self, callback): + ''' + Set error callback. It is called when error occurs. + + :param callback: function with one parameter for error message + ''' + + self._error_callback = callback + + # result handling + @property + def result_callback(self): + return self._result_callback + + @result_callback.setter + def result_callback(self, callback): + ''' + Set result callback. It is called when results are received. + + :param callback: function with one parameter for lists of strings + ''' + + self._result_callback = callback + + @property + def partial_result_callback(self): + return self._partial_result_callback + + @partial_result_callback.setter + def partial_result_callback(self, callback): + ''' + Set partial result callback. It is called when partial results are + received while the listener is still in listening mode. + + :param callback: function with one parameter for lists of strings + ''' + + self._partial_result_callback = callback + + # voice changes handling + @property + def volume_callback(self): + return self._volume_callback + + @volume_callback.setter + def volume_callback(self, callback): + ''' + Set volume voice callback. + + It is called when loudness of the voice changes. + + :param callback: function with one parameter for volume RMS dB (float). + ''' + self._volume_callback = callback + + # Implementation Java Interfaces + @java_method('()V') + def onBeginningOfSpeech(self): + pass + + @java_method('([B)V') + def onBufferReceived(self, buffer): + pass + + @java_method('()V') + def onEndOfSpeech(self): + pass + + @java_method('(I)V') + def onError(self, error): + msg = '' + if error == SpeechRecognizer.ERROR_AUDIO: + msg = 'audio' + if error == SpeechRecognizer.ERROR_CLIENT: + msg = 'client' + if error == SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS: + msg = 'insufficient_permissions' + if error == SpeechRecognizer.ERROR_NETWORK: + msg = 'network' + if error == SpeechRecognizer.ERROR_NETWORK_TIMEOUT: + msg = 'network_timeout' + if error == SpeechRecognizer.ERROR_NO_MATCH: + msg = 'no_match' + if error == SpeechRecognizer.ERROR_RECOGNIZER_BUSY: + msg = 'recognizer_busy' + if error == SpeechRecognizer.ERROR_SERVER: + msg = 'server' + if error == SpeechRecognizer.ERROR_SPEECH_TIMEOUT: + msg = 'speech_timeout' + + if msg and self.error_callback: + self.error_callback('error:' + msg) + + @java_method('(ILandroid/os/Bundle;)V') + def onEvent(self, event_type, params): + pass + + @java_method('(Landroid/os/Bundle;)V') + def onPartialResults(self, results): + texts = [] + matches = results.getStringArrayList(SpeechResults) + for match in matches.toArray(): + if isinstance(match, bytes): + match = match.decode('utf-8') + texts.append(match) + + if texts and self.partial_result_callback: + self.partial_result_callback(texts) + + @java_method('(Landroid/os/Bundle;)V') + def onReadyForSpeech(self, params): + pass + + @java_method('(Landroid/os/Bundle;)V') + def onResults(self, results): + texts = [] + matches = results.getStringArrayList(SpeechResults) + for match in matches.toArray(): + if isinstance(match, bytes): + match = match.decode('utf-8') + texts.append(match) + + if texts and self.result_callback: + self.result_callback(texts) + + @java_method('(F)V') + def onRmsChanged(self, rmsdB): + if self.volume_callback: + self.volume_callback(rmsdB) + + +class AndroidSpeech(STT): + ''' + Android Speech Implementation. + + Android class `SpeechRecognizer`'s listening deactivates automatically. + + Class methods `_on_error()`, `_on_result()` listeners. You can find + documentation here: + https://developer.android.com/reference/android/speech/RecognitionListener + ''' + + def _on_error(self, msg): + self.errors.append(msg) + self.stop() + + def _on_result(self, messages): + self.results.extend(messages) + self.stop() + + def _on_partial(self, messages): + self.partial_results.extend(messages) + + @run_on_ui_thread + def _start(self): + intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) + intent.putExtra( + RecognizerIntent.EXTRA_CALLING_PACKAGE, + activity.getPackageName() + ) + + # language preferences + intent.putExtra( + RecognizerIntent.EXTRA_LANGUAGE_PREFERENCE, self.language + ) + intent.putExtra( + RecognizerIntent.EXTRA_LANGUAGE_MODEL, + RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH + ) + + # results settings + intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1000) + intent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, True) + if self.prefer_offline: + intent.putExtra(RecognizerIntent.EXTRA_PREFER_OFFLINE, True) + + # listener and callbacks + listener = SpeechListener() + listener.error_callback = self._on_error + listener.result_callback = self._on_result + listener.partial_result_callback = self._on_partial + + # create recognizer and start + self.speech = SpeechRecognizer.createSpeechRecognizer(activity) + self.speech.setRecognitionListener(listener) + self.speech.startListening(intent) + + @run_on_ui_thread + def _stop(self): + if not self.speech: + return + + # stop listening + self.speech.stopListening() + + # free object + self.speech.destroy() + self.speech = None + + def _exist(self): + return bool( + SpeechRecognizer.isRecognitionAvailable(activity) + ) + + +def instance(): + return AndroidSpeech() diff --git a/sbapp/plyer/platforms/android/temperature.py b/sbapp/plyer/platforms/android/temperature.py new file mode 100644 index 0000000..fb05363 --- /dev/null +++ b/sbapp/plyer/platforms/android/temperature.py @@ -0,0 +1,66 @@ +from jnius import autoclass +from jnius import cast +from jnius import java_method +from jnius import PythonJavaClass + +from plyer.facades import Temperature +from plyer.platforms.android import activity + +ActivityInfo = autoclass('android.content.pm.ActivityInfo') +Context = autoclass('android.content.Context') +Sensor = autoclass('android.hardware.Sensor') +SensorManager = autoclass('android.hardware.SensorManager') + + +class TemperatureSensorListener(PythonJavaClass): + __javainterfaces__ = ['android/hardware/SensorEventListener'] + + def __init__(self): + super().__init__() + service = activity.getSystemService(Context.SENSOR_SERVICE) + self.SensorManager = cast('android.hardware.SensorManager', service) + + self.sensor = self.SensorManager.getDefaultSensor( + Sensor.TYPE_AMBIENT_TEMPERATURE) + self.value = None + + def enable(self): + self.SensorManager.registerListener( + self, self.sensor, + SensorManager.SENSOR_DELAY_NORMAL + ) + + def disable(self): + self.SensorManager.unregisterListener(self, self.sensor) + + @java_method('(Landroid/hardware/SensorEvent;)V') + def onSensorChanged(self, event): + self.value = event.values[0] + + @java_method('(Landroid/hardware/Sensor;I)V') + def onAccuracyChanged(self, sensor, accuracy): + pass + + +class AndroidTemperature(Temperature): + + listener = None + + def _get_temperature(self): + if self.listener and self.listener.value: + temperature = self.listener.value + return temperature + + def _enable(self): + if not self.listener: + self.listener = TemperatureSensorListener() + self.listener.enable() + + def _disable(self): + if self.listener: + self.listener.disable() + delattr(self, 'listener') + + +def instance(): + return AndroidTemperature() diff --git a/sbapp/plyer/platforms/android/tts.py b/sbapp/plyer/platforms/android/tts.py new file mode 100644 index 0000000..191dbbc --- /dev/null +++ b/sbapp/plyer/platforms/android/tts.py @@ -0,0 +1,34 @@ +from time import sleep +from jnius import autoclass +from plyer.facades import TTS +from plyer.platforms.android import activity + +Locale = autoclass('java.util.Locale') +TextToSpeech = autoclass('android.speech.tts.TextToSpeech') + + +class AndroidTextToSpeech(TTS): + def _speak(self, **kwargs): + tts = TextToSpeech(activity, None) + + tts.setLanguage(Locale.US) + + retries = 0 # First try rarely succeeds due to some timing issue + message = kwargs.get('message') + + # first try for while loop + speak_status = tts.speak( + message, TextToSpeech.QUEUE_FLUSH, None + ) + + # -1 indicates error. Let's wait and then try again + while retries < 100 and speak_status == -1: + sleep(0.1) + retries += 1 + speak_status = tts.speak( + message, TextToSpeech.QUEUE_FLUSH, None + ) + + +def instance(): + return AndroidTextToSpeech() diff --git a/sbapp/plyer/platforms/android/uniqueid.py b/sbapp/plyer/platforms/android/uniqueid.py new file mode 100644 index 0000000..fecd63f --- /dev/null +++ b/sbapp/plyer/platforms/android/uniqueid.py @@ -0,0 +1,28 @@ +''' +Module of Android API for plyer.uniqueid. +''' + +from jnius import autoclass +from plyer.platforms.android import activity +from plyer.facades import UniqueID + +Secure = autoclass('android.provider.Settings$Secure') + + +class AndroidUniqueID(UniqueID): + ''' + Implementation of Android uniqueid API. + ''' + + def _get_uid(self): + return Secure.getString( + activity.getContentResolver(), + Secure.ANDROID_ID + ) + + +def instance(): + ''' + Instance for facade proxy. + ''' + return AndroidUniqueID() diff --git a/sbapp/plyer/platforms/android/vibrator.py b/sbapp/plyer/platforms/android/vibrator.py new file mode 100644 index 0000000..a318c3c --- /dev/null +++ b/sbapp/plyer/platforms/android/vibrator.py @@ -0,0 +1,63 @@ +"""Implementation Vibrator for Android.""" + +from jnius import autoclass, cast +from plyer.facades import Vibrator +from plyer.platforms.android import activity +from plyer.platforms.android import SDK_INT + +Context = autoclass("android.content.Context") +vibrator_service = activity.getSystemService(Context.VIBRATOR_SERVICE) +vibrator = cast("android.os.Vibrator", vibrator_service) +if SDK_INT >= 26: + VibrationEffect = autoclass("android.os.VibrationEffect") + + +class AndroidVibrator(Vibrator): + """Android Vibrator class. + + Supported features: + * vibrate for some period of time. + * vibrate from given pattern. + * cancel vibration. + * check whether Vibrator exists. + """ + + def _vibrate(self, time=None, **kwargs): + if vibrator: + if SDK_INT >= 26: + vibrator.vibrate( + VibrationEffect.createOneShot( + int(1000 * time), VibrationEffect.DEFAULT_AMPLITUDE + ) + ) + else: + vibrator.vibrate(int(1000 * time)) + + def _pattern(self, pattern=None, repeat=None, **kwargs): + pattern = [int(1000 * time) for time in pattern] + + if vibrator: + if SDK_INT >= 26: + vibrator.vibrate( + VibrationEffect.createWaveform(pattern, repeat) + ) + else: + vibrator.vibrate(pattern, repeat) + + def _exists(self, **kwargs): + if SDK_INT >= 11: + return vibrator.hasVibrator() + elif vibrator_service is None: + raise NotImplementedError() + return True + + def _cancel(self, **kwargs): + vibrator.cancel() + + +def instance(): + """Returns Vibrator with android features. + + :return: instance of class AndroidVibrator + """ + return AndroidVibrator() diff --git a/sbapp/plyer/platforms/ios/__init__.py b/sbapp/plyer/platforms/ios/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sbapp/plyer/platforms/ios/accelerometer.py b/sbapp/plyer/platforms/ios/accelerometer.py new file mode 100644 index 0000000..7250384 --- /dev/null +++ b/sbapp/plyer/platforms/ios/accelerometer.py @@ -0,0 +1,34 @@ +''' +iOS accelerometer +----------------- + +Taken from: http://pyobjus.readthedocs.org/en/latest/pyobjus_ios.html \ + #accessing-accelerometer +''' + +from plyer.facades import Accelerometer +from pyobjus import autoclass + + +class IosAccelerometer(Accelerometer): + + def __init__(self): + super().__init__() + self.bridge = autoclass('bridge').alloc().init() + self.bridge.motionManager.setAccelerometerUpdateInterval_(0.1) + + def _enable(self): + self.bridge.startAccelerometer() + + def _disable(self): + self.bridge.stopAccelerometer() + + def _get_acceleration(self): + return ( + self.bridge.ac_x, + self.bridge.ac_y, + self.bridge.ac_z) + + +def instance(): + return IosAccelerometer() diff --git a/sbapp/plyer/platforms/ios/barometer.py b/sbapp/plyer/platforms/ios/barometer.py new file mode 100644 index 0000000..a5c5dc3 --- /dev/null +++ b/sbapp/plyer/platforms/ios/barometer.py @@ -0,0 +1,31 @@ +''' +iOS Barometer +------------- +''' + +from plyer.facades import Barometer +from pyobjus import autoclass + + +class iOSBarometer(Barometer): + + def __init__(self): + super().__init__() + self.bridge = autoclass('bridge').alloc().init() + + def _enable(self): + self.bridge.startRelativeAltitude() + + def _disable(self): + self.bridge.stopRelativeAltitude() + + def _get_pressure(self): + ''' + 1 kPa = 10 hPa + ''' + return ( + self.bridge.pressure * 10) + + +def instance(): + return iOSBarometer() diff --git a/sbapp/plyer/platforms/ios/battery.py b/sbapp/plyer/platforms/ios/battery.py new file mode 100644 index 0000000..818f0be --- /dev/null +++ b/sbapp/plyer/platforms/ios/battery.py @@ -0,0 +1,47 @@ +''' +Module of iOS API for plyer.battery. +''' + +from pyobjus import autoclass +from pyobjus.dylib_manager import load_framework +from plyer.facades import Battery + +load_framework('/System/Library/Frameworks/UIKit.framework') +UIDevice = autoclass('UIDevice') + + +class IOSBattery(Battery): + ''' + Implementation of iOS battery API. + ''' + + def __init__(self): + super().__init__() + self.device = UIDevice.currentDevice() + + def _get_state(self): + status = {"isCharging": None, "percentage": None} + + if not self.device.batteryMonitoringEnabled: + self.device.setBatteryMonitoringEnabled_(True) + + if self.device.batteryState == 0: + is_charging = None + elif self.device.batteryState == 2: + is_charging = True + else: + is_charging = False + + percentage = self.device.batteryLevel * 100. + + status['isCharging'] = is_charging + status['percentage'] = percentage + + return status + + +def instance(): + ''' + Instance for facade proxy. + ''' + return IOSBattery() diff --git a/sbapp/plyer/platforms/ios/brightness.py b/sbapp/plyer/platforms/ios/brightness.py new file mode 100644 index 0000000..065869a --- /dev/null +++ b/sbapp/plyer/platforms/ios/brightness.py @@ -0,0 +1,27 @@ +''' +iOS Brightness +-------------- +''' + +from pyobjus import autoclass +from plyer.facades import Brightness +from pyobjus.dylib_manager import load_framework + +load_framework('/System/Library/Frameworks/UIKit.framework') +UIScreen = autoclass('UIScreen') + + +class iOSBrightness(Brightness): + + def __init__(self): + self.screen = UIScreen.mainScreen() + + def _current_level(self): + return self.screen.brightness * 100 + + def set_level(self, level): + self.screen.brightness = level / 100 + + +def instance(): + return iOSBrightness() diff --git a/sbapp/plyer/platforms/ios/call.py b/sbapp/plyer/platforms/ios/call.py new file mode 100644 index 0000000..c751399 --- /dev/null +++ b/sbapp/plyer/platforms/ios/call.py @@ -0,0 +1,29 @@ +''' +IOS Call +---------- +''' + +from plyer.facades import Call +from pyobjus import autoclass, objc_str + +NSURL = autoclass('NSURL') +NSString = autoclass('NSString') +UIApplication = autoclass('UIApplication') + + +class IOSCall(Call): + + def _makecall(self, **kwargs): + tel = kwargs.get('tel') + url = "tel://" + tel + nsurl = NSURL.alloc().initWithString_(objc_str(url)) + + UIApplication.sharedApplication().openURL_(nsurl) + + def _dialcall(self, **kwargs): + pass + # Not possible, Access not provided by iPhone SDK + + +def instance(): + return IOSCall() diff --git a/sbapp/plyer/platforms/ios/camera.py b/sbapp/plyer/platforms/ios/camera.py new file mode 100644 index 0000000..dfe5265 --- /dev/null +++ b/sbapp/plyer/platforms/ios/camera.py @@ -0,0 +1,52 @@ +from os import remove +from plyer.facades import Camera + +from plyer.utils import reify + + +class iOSCamera(Camera): + + @reify + def photos(self): + # pyPhotoLibrary is a ios recipe/module that + # interacts with the gallery and the camera on ios. + from photolibrary import PhotosLibrary + return PhotosLibrary() + + def _take_picture(self, on_complete, filename=None): + assert(on_complete is not None) + self.on_complete = on_complete + self.filename = filename + photos = self.photos + + if not photos.isCameraAvailable(): + # no camera hardware + return False + + photos.bind(on_image_captured=self.capture_callback) + self._capture_filename = filename + photos.capture_image(filename) + return True + + def capture_callback(self, photolibrary): + # Image was chosen + + # unbind + self.photos.unbind(on_image_captured=self.capture_callback) + + if self.on_complete(self.filename): + self._remove(self.filename) + + def _take_video(self, on_complete, filename=None): + assert(on_complete is not None) + raise NotImplementedError + + def _remove(self, fn): + try: + remove(fn) + except OSError: + print('Could not remove photo!') + + +def instance(): + return iOSCamera() diff --git a/sbapp/plyer/platforms/ios/compass.py b/sbapp/plyer/platforms/ios/compass.py new file mode 100644 index 0000000..855484b --- /dev/null +++ b/sbapp/plyer/platforms/ios/compass.py @@ -0,0 +1,43 @@ +''' +iOS Compass +----------- +''' + +from plyer.facades import Compass +from pyobjus import autoclass + + +class IosCompass(Compass): + + def __init__(self): + super().__init__() + self.bridge = autoclass('bridge').alloc().init() + self.bridge.motionManager.setMagnetometerUpdateInterval_(0.1) + self.bridge.motionManager.setDeviceMotionUpdateInterval_(0.1) + + def _enable(self): + self.bridge.startMagnetometer() + self.bridge.startDeviceMotionWithReferenceFrame() + + def _disable(self): + self.bridge.stopMagnetometer() + self.bridge.stopDeviceMotion() + + def _get_orientation(self): + return ( + self.bridge.mf_x, + self.bridge.mf_y, + self.bridge.mf_z) + + def _get_field_uncalib(self): + return ( + self.bridge.mg_x, + self.bridge.mg_y, + self.bridge.mg_z, + self.bridge.mg_x - self.bridge.mf_x, + self.bridge.mg_y - self.bridge.mf_y, + self.bridge.mg_z - self.bridge.mf_z) + + +def instance(): + return IosCompass() diff --git a/sbapp/plyer/platforms/ios/email.py b/sbapp/plyer/platforms/ios/email.py new file mode 100644 index 0000000..e1cc7cb --- /dev/null +++ b/sbapp/plyer/platforms/ios/email.py @@ -0,0 +1,52 @@ +''' +Module of iOS API for plyer.email. +''' + +try: + from urllib.parse import quote +except ImportError: + from urllib import quote + +from plyer.facades import Email +from pyobjus import autoclass, objc_str +from pyobjus.dylib_manager import load_framework + +load_framework('/System/Library/Frameworks/UIKit.framework') + +NSURL = autoclass('NSURL') +NSString = autoclass('NSString') +UIApplication = autoclass('UIApplication') + + +class IOSEmail(Email): + ''' + Implementation of iOS battery API. + ''' + + def _send(self, **kwargs): + recipient = kwargs.get('recipient') + subject = kwargs.get('subject') + text = kwargs.get('text') + + uri = "mailto:" + if recipient: + uri += str(recipient) + if subject: + uri += "?" if "?" not in uri else "&" + uri += "subject=" + uri += quote(str(subject)) + if text: + uri += "?" if "?" not in uri else "&" + uri += "body=" + uri += quote(str(text)) + + nsurl = NSURL.alloc().initWithString_(objc_str(uri)) + + UIApplication.sharedApplication().openURL_(nsurl) + + +def instance(): + ''' + Instance for facade proxy. + ''' + return IOSEmail() diff --git a/sbapp/plyer/platforms/ios/filechooser.py b/sbapp/plyer/platforms/ios/filechooser.py new file mode 100644 index 0000000..ba49ba9 --- /dev/null +++ b/sbapp/plyer/platforms/ios/filechooser.py @@ -0,0 +1,81 @@ +''' +IOS file chooser +-------------------- + +This module houses the iOS implementation of the plyer FileChooser. + +.. versionadded:: 1.4.4 +''' + +from plyer.facades import FileChooser +from pyobjus import autoclass, protocol +from pyobjus.dylib_manager import load_framework + + +load_framework('/System/Library/Frameworks/Photos.framework') + + +class IOSFileChooser(FileChooser): + ''' + FileChooser implementation for IOS using + the built-in file browser via UIImagePickerController. + + .. versionadded:: 1.4.0 + ''' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._on_selection = None + + def _file_selection_dialog(self, *args, **kwargs): + """ + Function called when action is required, A "mode" parameter specifies + which and is one of "open", "save" or "dir". + """ + self._on_selection = kwargs["on_selection"] + if kwargs["mode"] == "open": + self._open() + else: + raise NotImplementedError() + + def _get_picker(self): + """ + Return an instantiated and configured UIImagePickerController. + """ + picker = autoclass("UIImagePickerController") + po = picker.alloc().init() + po.sourceType = 0 + po.delegate = self + return po + + def _open(self): + """ + Launch the native iOS file browser. Upon selection, the + `imagePickerController_didFinishPickingMediaWithInfo_` delegate is + called where we close the file browser and handle the result. + """ + picker = self._get_picker() + UIApplication = autoclass('UIApplication') + vc = UIApplication.sharedApplication().keyWindow.rootViewController() + vc.presentViewController_animated_completion_(picker, True, None) + + @protocol('UIImagePickerControllerDelegate') + def imagePickerController_didFinishPickingMediaWithInfo_( + self, image_picker, frozen_dict): + """ + Delegate which handles the result of the image selection process. + """ + image_picker.dismissViewControllerAnimated_completion_(True, None) + + # Note: We need to call this Objective C class as there is currently + # no way to call a non-class function via pyobjus. And here, + # we have to use the `UIImagePNGRepresentation` to get the png + # representation. For this, please ensure you are using an + # appropriate version of kivy-ios. + native_image_picker = autoclass("NativeImagePicker").alloc().init() + path = native_image_picker.writeToPNG_(frozen_dict) + self._on_selection([path.UTF8String()]) + + +def instance(): + return IOSFileChooser() diff --git a/sbapp/plyer/platforms/ios/flash.py b/sbapp/plyer/platforms/ios/flash.py new file mode 100644 index 0000000..e4bedac --- /dev/null +++ b/sbapp/plyer/platforms/ios/flash.py @@ -0,0 +1,50 @@ +# coding=utf-8 +""" +Flash +----- +""" +from plyer.facades import Flash +from pyobjus import autoclass + +NSString = autoclass("NSString") +AVCaptureDevice = autoclass("AVCaptureDevice") +AVMediaTypeVideo = NSString.alloc().initWithUTF8String_("vide") +AVCaptureTorchModeOff = 0 +AVCaptureTorchModeOn = 1 + + +class IosFlash(Flash): + _camera = None + + def _on(self): + if self._camera is None: + self._camera_open() + if not self._camera: + return + self._camera.lockForConfiguration_(None) + try: + self._camera.setTorchMode(AVCaptureTorchModeOn) + finally: + self._camera.unlockForConfiguration() + + def _off(self): + if not self._camera: + return + self._camera.lockForConfiguration_(None) + try: + self._camera.setTorchMode(AVCaptureTorchModeOff) + finally: + self._camera.unlockForConfiguration() + + def _release(self): + pass + + def _camera_open(self): + device = AVCaptureDevice.defaultDeviceWithMediaType_(AVMediaTypeVideo) + if not device: + return + self._camera = device + + +def instance(): + return IosFlash() diff --git a/sbapp/plyer/platforms/ios/gps.py b/sbapp/plyer/platforms/ios/gps.py new file mode 100644 index 0000000..4c52738 --- /dev/null +++ b/sbapp/plyer/platforms/ios/gps.py @@ -0,0 +1,80 @@ +''' +iOS GPS +----------- +''' + +from pyobjus import autoclass, protocol +from pyobjus.dylib_manager import load_framework +from plyer.facades import GPS + +load_framework('/System/Library/Frameworks/CoreLocation.framework') +CLLocationManager = autoclass('CLLocationManager') + + +class IosGPS(GPS): + def _configure(self): + if not hasattr(self, '_location_manager'): + self._location_manager = CLLocationManager.alloc().init() + + def _start(self, **kwargs): + self._location_manager.delegate = self + + self._location_manager.requestWhenInUseAuthorization() + # NSLocationWhenInUseUsageDescription key must exist in Info.plist + # file. When the authorization prompt is displayed your app goes + # into pause mode and if your app doesn't support background mode + # it will crash. + self._location_manager.startUpdatingLocation() + + def _stop(self): + self._location_manager.stopUpdatingLocation() + + @protocol('CLLocationManagerDelegate') + def locationManager_didChangeAuthorizationStatus_(self, manager, status): + if self.on_status: + s_status = '' + provider_status = '' + provider = 'standard-ios-provider' + if status == 0: + provider_status = 'provider-disabled' + s_status = 'notDetermined' + elif status == 1: + provider_status = 'provider-enabled' + s_status = 'restricted' + elif status == 2: + provider_status = 'provider-disabled' + s_status = 'denied' + elif status == 3: + provider_status = 'provider-enabled' + s_status = 'authorizedAlways' + elif status == 4: + provider_status = 'provider-enabled' + s_status = 'authorizedWhenInUse' + self.on_status(provider_status, '{}: {}'.format( + provider, s_status)) + + @protocol('CLLocationManagerDelegate') + def locationManager_didUpdateLocations_(self, manager, locations): + location = manager.location + + description = location.description.UTF8String() + split_description = description.split('<')[-1].split('>')[0].split(',') + + lat, lon = [float(coord) for coord in split_description] + acc = float(description.split(' +/- ')[-1].split('m ')[0]) + + speed = location.speed + altitude = location.altitude + course = location.course + + self.on_location( + lat=lat, + lon=lon, + speed=speed, + bearing=course, + altitude=altitude, + accuracy=acc) + + +def instance(): + return IosGPS() diff --git a/sbapp/plyer/platforms/ios/gravity.py b/sbapp/plyer/platforms/ios/gravity.py new file mode 100644 index 0000000..a2b1be0 --- /dev/null +++ b/sbapp/plyer/platforms/ios/gravity.py @@ -0,0 +1,31 @@ +''' +iOS Gravity +----------- + +''' + +from plyer.facades import Gravity +from pyobjus import autoclass + + +class iOSGravity(Gravity): + + def __init__(self): + self.bridge = autoclass('bridge').alloc().init() + self.bridge.motionManager.setDeviceMotionUpdateInterval_(0.1) + + def _enable(self): + self.bridge.startDeviceMotion() + + def _disable(self): + self.bridge.stopDeviceMotion() + + def _get_gravity(self): + return ( + self.bridge.g_x, + self.bridge.g_y, + self.bridge.g_z) + + +def instance(): + return iOSGravity() diff --git a/sbapp/plyer/platforms/ios/gyroscope.py b/sbapp/plyer/platforms/ios/gyroscope.py new file mode 100644 index 0000000..340653c --- /dev/null +++ b/sbapp/plyer/platforms/ios/gyroscope.py @@ -0,0 +1,55 @@ +''' +iOS Gyroscope +--------------------- +''' + +from plyer.facades import Gyroscope +from pyobjus import autoclass + +from pyobjus.dylib_manager import load_framework + +load_framework('/System/Library/Frameworks/UIKit.framework') +UIDevice = autoclass('UIDevice') + +device = UIDevice.currentDevice() + + +class IosGyroscope(Gyroscope): + + def __init__(self): + super().__init__() + self.bridge = autoclass('bridge').alloc().init() + + if int(device.systemVersion.UTF8String().split('.')[0]) <= 4: + self.bridge.motionManager.setGyroscopeUpdateInterval_(0.1) + else: + self.bridge.motionManager.setGyroUpdateInterval_(0.1) + + self.bridge.motionManager.setDeviceMotionUpdateInterval_(0.1) + + def _enable(self): + self.bridge.startGyroscope() + self.bridge.startDeviceMotion() + + def _disable(self): + self.bridge.stopGyroscope() + self.bridge.stopDeviceMotion() + + def _get_orientation(self): + return ( + self.bridge.rotation_rate_x, + self.bridge.rotation_rate_y, + self.bridge.rotation_rate_z) + + def _get_rotation_uncalib(self): + return ( + self.bridge.gy_x, + self.bridge.gy_y, + self.bridge.gy_z, + self.bridge.gy_x - self.bridge.rotation_rate_x, + self.bridge.gy_y - self.bridge.rotation_rate_y, + self.bridge.gy_z - self.bridge.rotation_rate_z) + + +def instance(): + return IosGyroscope() diff --git a/sbapp/plyer/platforms/ios/keystore.py b/sbapp/plyer/platforms/ios/keystore.py new file mode 100644 index 0000000..289a5c0 --- /dev/null +++ b/sbapp/plyer/platforms/ios/keystore.py @@ -0,0 +1,23 @@ +from plyer.facades import Keystore +from pyobjus import autoclass, objc_str + +NSUserDefaults = autoclass('NSUserDefaults') + + +class IosKeystore(Keystore): + + def _set_key(self, servicename, key, value, **kwargs): + NSUserDefaults.standardUserDefaults().setObject_forKey_( + objc_str(value), objc_str(key)) + + def _get_key(self, servicename, key, **kwargs): + ret = NSUserDefaults.standardUserDefaults().stringForKey_( + objc_str(key)) + if ret is not None: + return ret.UTF8String() + else: + return ret + + +def instance(): + return IosKeystore() diff --git a/sbapp/plyer/platforms/ios/sms.py b/sbapp/plyer/platforms/ios/sms.py new file mode 100644 index 0000000..ef3acaa --- /dev/null +++ b/sbapp/plyer/platforms/ios/sms.py @@ -0,0 +1,43 @@ +''' +IOS Sms +---------- +''' + +from plyer.facades import Sms +from pyobjus import autoclass, objc_str +from pyobjus.dylib_manager import load_framework + +NSURL = autoclass('NSURL') +NSString = autoclass('NSString') +UIApplication = autoclass('UIApplication') +load_framework('/System/Library/Frameworks/MessageUI.framework') + + +class IOSSms(Sms): + + def _send(self, **kwargs): + ''' + This method provides sending messages to recipients. + + Expects 2 parameters in kwargs: + - recipient: String type + - message: String type + + Opens a message interface with recipient and message information. + ''' + recipient = kwargs.get('recipient') + message = kwargs.get('message') + url = "sms:" + if recipient: + # Apple has not supported multiple recipients yet. + url += str(recipient) + if message: + # Apple has to supported it yet. + pass + + nsurl = NSURL.alloc().initWithString_(objc_str(url)) + UIApplication.sharedApplication().openURL_(nsurl) + + +def instance(): + return IOSSms() diff --git a/sbapp/plyer/platforms/ios/spatialorientation.py b/sbapp/plyer/platforms/ios/spatialorientation.py new file mode 100644 index 0000000..d42d0fa --- /dev/null +++ b/sbapp/plyer/platforms/ios/spatialorientation.py @@ -0,0 +1,31 @@ +''' +iOS Spatial Orientation +----------------------- + +''' + +from plyer.facades import SpatialOrientation +from pyobjus import autoclass + + +class iOSSpatialOrientation(SpatialOrientation): + + def __init__(self): + self.bridge = autoclass('bridge').alloc().init() + self.bridge.motionManager.setDeviceMotionUpdateInterval_(0.1) + + def _enable_listener(self): + self.bridge.startDeviceMotion() + + def _disable_listener(self): + self.bridge.stopDeviceMotion() + + def _get_orientation(self): + return ( + self.bridge.sp_yaw, + self.bridge.sp_pitch, + self.bridge.sp_roll) + + +def instance(): + return iOSSpatialOrientation() diff --git a/sbapp/plyer/platforms/ios/storagepath.py b/sbapp/plyer/platforms/ios/storagepath.py new file mode 100644 index 0000000..cd8bbc3 --- /dev/null +++ b/sbapp/plyer/platforms/ios/storagepath.py @@ -0,0 +1,62 @@ +''' +iOS Storage Path +-------------------- +''' + +from plyer.facades import StoragePath +from pyobjus import autoclass +import os + +NSFileManager = autoclass('NSFileManager') + +# Directory constants (NSSearchPathDirectory enumeration) +NSApplicationDirectory = 1 +NSDocumentDirectory = 9 +NSDownloadsDirectory = 15 +NSMoviesDirectory = 17 +NSMusicDirectory = 18 +NSPicturesDirectory = 19 + + +class iOSStoragePath(StoragePath): + + def __init__(self): + self.defaultManager = NSFileManager.defaultManager() + + def _get_home_dir(self): + return os.path.expanduser('~/') + + def _get_external_storage_dir(self): + return 'This feature is not implemented for this platform.' + + def _get_root_dir(self): + return 'This feature is not implemented for this platform.' + + def _get_documents_dir(self): + return self.defaultManager.URLsForDirectory_inDomains_( + NSDocumentDirectory, 1).firstObject().absoluteString.UTF8String() + + def _get_downloads_dir(self): + return self.defaultManager.URLsForDirectory_inDomains_( + NSDownloadsDirectory, 1).firstObject().absoluteString.UTF8String() + + def _get_videos_dir(self): + return self.defaultManager.URLsForDirectory_inDomains_( + NSMoviesDirectory, 1).firstObject().absoluteString.UTF8String() + + def _get_music_dir(self): + return self.defaultManager.URLsForDirectory_inDomains_( + NSMusicDirectory, 1).firstObject().absoluteString.UTF8String() + + def _get_pictures_dir(self): + return self.defaultManager.URLsForDirectory_inDomains_( + NSPicturesDirectory, 1).firstObject().absoluteString.UTF8String() + + def _get_application_dir(self): + return self.defaultManager.URLsForDirectory_inDomains_( + NSApplicationDirectory, 1).firstObject().absoluteString.\ + UTF8String() + + +def instance(): + return iOSStoragePath() diff --git a/sbapp/plyer/platforms/ios/tts.py b/sbapp/plyer/platforms/ios/tts.py new file mode 100644 index 0000000..046769f --- /dev/null +++ b/sbapp/plyer/platforms/ios/tts.py @@ -0,0 +1,37 @@ +from pyobjus import autoclass, objc_str +from pyobjus.dylib_manager import load_framework + +from plyer.facades import TTS + +load_framework('/System/Library/Frameworks/AVFoundation.framework') +AVSpeechUtterance = autoclass('AVSpeechUtterance') +AVSpeechSynthesizer = autoclass('AVSpeechSynthesizer') +AVSpeechSynthesisVoice = autoclass('AVSpeechSynthesisVoice') + + +class iOSTextToSpeech(TTS): + def __init__(self): + super().__init__() + self.synth = AVSpeechSynthesizer.alloc().init() + self.voice = None + + def _set_locale(self, locale="en-US"): + self.voice = AVSpeechSynthesisVoice.voiceWithLanguage_( + objc_str(locale) + ) + + def _speak(self, **kwargs): + message = kwargs.get('message') + + if(not self.voice): + self._set_locale() + + utterance = \ + AVSpeechUtterance.speechUtteranceWithString_(objc_str(message)) + + utterance.voice = self.voice + self.synth.speakUtterance_(utterance) + + +def instance(): + return iOSTextToSpeech() diff --git a/sbapp/plyer/platforms/ios/uniqueid.py b/sbapp/plyer/platforms/ios/uniqueid.py new file mode 100644 index 0000000..4d30249 --- /dev/null +++ b/sbapp/plyer/platforms/ios/uniqueid.py @@ -0,0 +1,27 @@ +''' +Module of iOS API for plyer.uniqueid. +''' + +from pyobjus import autoclass +from pyobjus.dylib_manager import load_framework +from plyer.facades import UniqueID + +load_framework('/System/Library/Frameworks/UIKit.framework') +UIDevice = autoclass('UIDevice') + + +class IOSUniqueID(UniqueID): + ''' + Implementation of iOS uniqueid API. + ''' + + def _get_uid(self): + uuid = UIDevice.currentDevice().identifierForVendor.UUIDString() + return uuid.UTF8String() + + +def instance(): + ''' + Instance for facade proxy. + ''' + return IOSUniqueID() diff --git a/sbapp/plyer/platforms/ios/vibrator.py b/sbapp/plyer/platforms/ios/vibrator.py new file mode 100644 index 0000000..922182c --- /dev/null +++ b/sbapp/plyer/platforms/ios/vibrator.py @@ -0,0 +1,43 @@ +'''Implementation Vibrator for iOS. + +Install: Add AudioToolbox framework to your application. +''' + +import ctypes +from plyer.facades import Vibrator + + +class IosVibrator(Vibrator): + '''iOS Vibrator class. + + iOS doesn't support any feature. + All time, pattern, repetition are ignored. + ''' + + def __init__(self): + super().__init__() + try: + self._func = ctypes.CDLL(None).AudioServicesPlaySystemSound + except AttributeError: + self._func = None + + def _vibrate(self, time=None, **kwargs): + # kSystemSoundID_Vibrate is 0x00000FFF + self._func(0xFFF) + + def _pattern(self, pattern=None, repeat=None, **kwargs): + self._vibrate() + + def _exists(self, **kwargs): + return self._func is not None + + def _cancel(self, **kwargs): + pass + + +def instance(): + '''Returns Vibrator + + :return: instance of class IosVibrator + ''' + return IosVibrator() diff --git a/sbapp/plyer/platforms/linux/__init__.py b/sbapp/plyer/platforms/linux/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sbapp/plyer/platforms/linux/accelerometer.py b/sbapp/plyer/platforms/linux/accelerometer.py new file mode 100644 index 0000000..7af283b --- /dev/null +++ b/sbapp/plyer/platforms/linux/accelerometer.py @@ -0,0 +1,35 @@ +''' +Linux accelerometer +--------------------- +''' + +from plyer.facades import Accelerometer +import glob +import re + + +class LinuxAccelerometer(Accelerometer): + + def _enable(self): + pass + + def _disable(self): + pass + + def _get_acceleration(self): + try: + pos = glob.glob("/sys/devices/platform/*/position")[0] + except IndexError: + raise Exception('Could not enable accelerometer!') + + with open(pos, "r") as p: + t = p.read() + coords = re.findall(r"[-]?\d+\.?\d*", t) + # Apparently the acceleration on sysfs goes from -1000 to 1000. + # I divide it by 100 to make it equivalent to Android. + # The negative is because the coordinates are inverted on Linux + return [float(i) / -100 for i in coords] + + +def instance(): + return LinuxAccelerometer() diff --git a/sbapp/plyer/platforms/linux/battery.py b/sbapp/plyer/platforms/linux/battery.py new file mode 100644 index 0000000..fa96592 --- /dev/null +++ b/sbapp/plyer/platforms/linux/battery.py @@ -0,0 +1,102 @@ +''' +Module of Linux API for plyer.battery. +''' + +from math import floor +from os import environ +from os.path import exists, join +from subprocess import Popen, PIPE +from plyer.facades import Battery +from plyer.utils import whereis_exe + + +class LinuxBattery(Battery): + ''' + Implementation of Linux battery API via accessing the sysclass power_supply + path from the kernel. + ''' + + def _get_state(self): + status = {"isCharging": None, "percentage": None} + + kernel_bat_path = join('/sys', 'class', 'power_supply', 'BAT0') + uevent = join(kernel_bat_path, 'uevent') + + with open(uevent) as fle: + lines = [ + line.decode('utf-8').strip() + for line in fle.readlines() + ] + output = { + line.split('=')[0]: line.split('=')[1] + for line in lines + } + + is_charging = output['POWER_SUPPLY_STATUS'] == 'Charging' + total = float(output['POWER_SUPPLY_CHARGE_FULL']) + now = float(output['POWER_SUPPLY_CHARGE_NOW']) + + capacity = floor(now / total * 100) + + status['percentage'] = capacity + status['isCharging'] = is_charging + return status + + +class UPowerBattery(Battery): + ''' + Implementation of UPower battery API. + ''' + + def _get_state(self): + # if no LANG specified, return empty string + old_lang = environ.get('LANG', '') + environ['LANG'] = 'C' + status = {"isCharging": None, "percentage": None} + + # We are supporting only one battery now + # this will fail if there is no object with such path, + # however it's safer than 'upower -d' which provides + # multiple unrelated 'state' and 'percentage' keywords + dev = "/org/freedesktop/UPower/devices/battery_BAT0" + upower_process = Popen( + ["upower", "--show-info", dev], + stdout=PIPE + ) + output = upower_process.communicate()[0].decode() + environ['LANG'] = old_lang + if not output: + return status + state = percentage = None + + for line in output.splitlines(): + if 'state' in line: + state = line.rpartition(':')[-1].strip() + + if 'percentage' in line: + percentage = line.rpartition(':')[-1].strip()[:-1] + + # switching decimal comma to dot + # (different LC_NUMERIC locale) + percentage = float( + percentage.replace(',', '.') + ) + + if state: + status['isCharging'] = state == "charging" + status['percentage'] = percentage + return status + + +def instance(): + ''' + Instance for facade proxy. + ''' + import sys + if whereis_exe('upower'): + return UPowerBattery() + sys.stderr.write("upower not found.") + + if exists(join('/sys', 'class', 'power_supply', 'BAT0')): + return LinuxBattery() + return Battery() diff --git a/sbapp/plyer/platforms/linux/brightness.py b/sbapp/plyer/platforms/linux/brightness.py new file mode 100755 index 0000000..0de1698 --- /dev/null +++ b/sbapp/plyer/platforms/linux/brightness.py @@ -0,0 +1,29 @@ +''' +Linux Brightness +---------------- + +''' + +from plyer.facades import Brightness +import subprocess +import os + + +class LinuxBrightness(Brightness): + + def __init__(self): + if os.system("which xbacklight"): + msg = ("It looks like 'xbacklight' is not installed. Try " + "installing it with your distribution's package manager.") + raise Exception(msg) + + def _current_level(self): + cr_level = subprocess.check_output(["xbacklight", "-get"]) + return str(cr_level) + + def _set_level(self, level): + subprocess.call(["xbacklight", "-set", str(level)]) + + +def instance(): + return LinuxBrightness() diff --git a/sbapp/plyer/platforms/linux/cpu.py b/sbapp/plyer/platforms/linux/cpu.py new file mode 100644 index 0000000..049a2d4 --- /dev/null +++ b/sbapp/plyer/platforms/linux/cpu.py @@ -0,0 +1,116 @@ +''' +Module of Linux API for plyer.cpu. +''' + +from os.path import join +from os import environ, listdir +from subprocess import Popen, PIPE +from plyer.facades import CPU +from plyer.utils import whereis_exe + + +class LinuxCPU(CPU): + ''' + Implementation of Linux CPU API. + ''' + + def _sockets(self): + # physical CPU sockets (or slots) on motherboard + sockets = [] # list of CPU ids from kernel + + # open Linux kernel data file for CPU + with open('/proc/cpuinfo', 'rb') as fle: + lines = fle.readlines() + + # go through the lines and obtain physical CPU ids + for line in lines: + line = line.decode('utf-8') + if 'physical id' not in line: + continue + cpuid = line.split(':')[1].strip() + sockets.append(cpuid) + + # total sockets is the length of unique CPU ids from kernel + sockets = len(set(sockets)) + return sockets + + def _physical(self): + # cores + physical = [] # list of CPU ids from kernel + + # open Linux kernel data file for CPU + with open('/proc/cpuinfo', 'rb') as fle: + lines = fle.readlines() + + # go through the lines and obtain CPU core ids + for line in lines: + line = line.decode('utf-8') + if 'core id' not in line: + continue + cpuid = line.split(':')[1].strip() + physical.append(cpuid) + + # total cores (socket * core per socket) + # is the length of unique CPU core ids from kernel + physical = len(set(physical)) + return physical + + def _logical(self): + # cores * threads + logical = None + old_lang = environ.get('LANG', '') + environ['LANG'] = 'C' + + _logical = Popen(['nproc', '--all'], stdout=PIPE) + output = _logical.communicate()[0].decode('utf-8').strip() + if output: + logical = int(output) + + environ['LANG'] = old_lang + return logical + + def _cache(self): + values = {key: 0 for key in ('L1', 'L2', 'L3')} + cpu_path = join('/sys', 'devices', 'system', 'cpu') + + # get present cores from kernel device + with open(join(cpu_path, 'present')) as fle: + present = fle.read().decode('utf-8') + present = present.strip().split('-') + + if len(present) == 2: + present = range(int(present[1]) + 1) + else: + present = [present[0]] + + cores = ['cpu{}'.format(i) for i in present] + for core in cores: + indicies = [ + # get 'indexN' files from 'cache' folder assuming + # the filename is in range index0 to index99 + # in case a wild 'index_whatevercontent' file appears + fle for fle in listdir(join(cpu_path, core, 'cache')) + if fle.startswith('index') and len(fle) <= len('index') + 2 + ] + + for index in indicies: + index_type = join(cpu_path, core, 'cache', index, 'level') + with open(index_type, 'rb') as fle: + cache_level = fle.read().decode('utf-8').strip() + values['L{}'.format(cache_level)] += 1 + return values + + @staticmethod + def _numa(): + return + + +def instance(): + ''' + Instance for facade proxy. + ''' + import sys + if whereis_exe('nproc'): + return LinuxCPU() + sys.stderr.write("nproc not found.") + return CPU() diff --git a/sbapp/plyer/platforms/linux/devicename.py b/sbapp/plyer/platforms/linux/devicename.py new file mode 100644 index 0000000..8941379 --- /dev/null +++ b/sbapp/plyer/platforms/linux/devicename.py @@ -0,0 +1,23 @@ +''' +Module of Linux API for plyer.devicename. +''' + +import socket +from plyer.facades import DeviceName + + +class LinuxDeviceName(DeviceName): + ''' + Implementation of Linux DeviceName API. + ''' + + def _get_device_name(self): + hostname = socket.gethostname() + return hostname + + +def instance(): + ''' + Instance for facade proxy. + ''' + return LinuxDeviceName() diff --git a/sbapp/plyer/platforms/linux/email.py b/sbapp/plyer/platforms/linux/email.py new file mode 100644 index 0000000..64cbb6d --- /dev/null +++ b/sbapp/plyer/platforms/linux/email.py @@ -0,0 +1,47 @@ +''' +Module of Linux API for plyer.email. +''' + +import subprocess +try: + from urllib.parse import quote +except ImportError: + from urllib import quote +from plyer.facades import Email +from plyer.utils import whereis_exe + + +class LinuxEmail(Email): + ''' + Implementation of Linux email API. + ''' + + def _send(self, **kwargs): + recipient = kwargs.get('recipient') + subject = kwargs.get('subject') + text = kwargs.get('text') + + uri = "mailto:" + if recipient: + uri += str(recipient) + if subject: + uri += "?" if "?" not in uri else "&" + uri += "subject=" + uri += quote(str(subject)) + if text: + uri += "?" if "?" not in uri else "&" + uri += "body=" + uri += quote(str(text)) + + subprocess.Popen(["xdg-open", uri]) + + +def instance(): + ''' + Instance for facade proxy. + ''' + import sys + if whereis_exe('xdg-open'): + return LinuxEmail() + sys.stderr.write("xdg-open not found.") + return Email() diff --git a/sbapp/plyer/platforms/linux/filechooser.py b/sbapp/plyer/platforms/linux/filechooser.py new file mode 100644 index 0000000..cfdb1ae --- /dev/null +++ b/sbapp/plyer/platforms/linux/filechooser.py @@ -0,0 +1,269 @@ +''' +Linux file chooser +------------------ +''' + +from plyer.facades import FileChooser +from distutils.spawn import find_executable as which +import os +import subprocess as sp +import time + + +class SubprocessFileChooser: + '''A file chooser implementation that allows using + subprocess back-ends. + Normally you only need to override _gen_cmdline, executable, + separator and successretcode. + ''' + + executable = "" + '''The name of the executable of the back-end. + ''' + + separator = "|" + '''The separator used by the back-end. Override this for automatic + splitting, or override _split_output. + ''' + + successretcode = 0 + '''The return code which is returned when the user doesn't close the + dialog without choosing anything, or when the app doesn't crash. + ''' + + path = None + multiple = False + filters = [] + preview = False + title = None + icon = None + show_hidden = False + + def __init__(self, *args, **kwargs): + self._handle_selection = kwargs.pop( + 'on_selection', self._handle_selection + ) + + # Simulate Kivy's behavior + for i in kwargs: + setattr(self, i, kwargs[i]) + + @staticmethod + def _handle_selection(selection): + ''' + Dummy placeholder for returning selection from chooser. + ''' + return selection + + _process = None + + def _run_command(self, cmd): + self._process = sp.Popen(cmd, stdout=sp.PIPE) + while True: + ret = self._process.poll() + if ret is not None: + if ret == self.successretcode: + out = self._process.communicate()[0].strip().decode('utf8') + return self._set_and_return_selection( + self._split_output(out)) + else: + return self._set_and_return_selection(None) + time.sleep(0.1) + + def _set_and_return_selection(self, value): + self.selection = value + self._handle_selection(value) + return value + + def _split_output(self, out): + '''This methods receives the output of the back-end and turns + it into a list of paths. + ''' + return out.split(self.separator) + + def _gen_cmdline(self): + '''Returns the command line of the back-end, based on the current + properties. You need to override this. + ''' + raise NotImplementedError() + + def run(self): + return self._run_command(self._gen_cmdline()) + + +class ZenityFileChooser(SubprocessFileChooser): + '''A FileChooser implementation using Zenity (on GNU/Linux). + + Not implemented features: + * show_hidden + * preview + ''' + + executable = "zenity" + separator = "|" + successretcode = 0 + + def _gen_cmdline(self): + cmdline = [ + which(self.executable), + "--file-selection", + "--confirm-overwrite" + ] + if self.multiple: + cmdline += ["--multiple"] + if self.mode == "save": + cmdline += ["--save"] + elif self.mode == "dir": + cmdline += ["--directory"] + if self.path: + cmdline += ["--filename", self.path] + if self.title: + cmdline += ["--name", self.title] + if self.icon: + cmdline += ["--window-icon", self.icon] + for f in self.filters: + if type(f) == str: + cmdline += ["--file-filter", f] + else: + cmdline += [ + "--file-filter", + "{name} | {flt}".format(name=f[0], flt=" ".join(f[1:])) + ] + return cmdline + + +class KDialogFileChooser(SubprocessFileChooser): + '''A FileChooser implementation using KDialog (on GNU/Linux). + + Not implemented features: + * show_hidden + * preview + ''' + + executable = "kdialog" + separator = "\n" + successretcode = 0 + + def _gen_cmdline(self): + cmdline = [which(self.executable)] + + filt = [] + + for f in self.filters: + if type(f) == str: + filt += [f] + else: + filt += list(f[1:]) + + if self.mode == "dir": + cmdline += [ + "--getexistingdirectory", + (self.path if self.path else os.path.expanduser("~")) + ] + elif self.mode == "save": + cmdline += [ + "--getsavefilename", + (self.path if self.path else os.path.expanduser("~")), + " ".join(filt) + ] + else: + cmdline += [ + "--getopenfilename", + (self.path if self.path else os.path.expanduser("~")), + " ".join(filt) + ] + if self.multiple: + cmdline += ["--multiple", "--separate-output"] + if self.title: + cmdline += ["--title", self.title] + if self.icon: + cmdline += ["--icon", self.icon] + return cmdline + + +class YADFileChooser(SubprocessFileChooser): + '''A NativeFileChooser implementation using YAD (on GNU/Linux). + + Not implemented features: + * show_hidden + ''' + + executable = "yad" + separator = "|?|" + successretcode = 0 + + def _gen_cmdline(self): + cmdline = [ + which(self.executable), + "--file-selection", + "--confirm-overwrite", + "--geometry", + "800x600+150+150" + ] + if self.multiple: + cmdline += ["--multiple", "--separator", self.separator] + if self.mode == "save": + cmdline += ["--save"] + elif self.mode == "dir": + cmdline += ["--directory"] + if self.preview: + cmdline += ["--add-preview"] + if self.path: + cmdline += ["--filename", self.path] + if self.title: + cmdline += ["--name", self.title] + if self.icon: + cmdline += ["--window-icon", self.icon] + for f in self.filters: + if type(f) == str: + cmdline += ["--file-filter", f] + else: + cmdline += [ + "--file-filter", + "{name} | {flt}".format(name=f[0], flt=" ".join(f[1:])) + ] + return cmdline + + +CHOOSERS = { + "gnome": ZenityFileChooser, + "kde": KDialogFileChooser, + "yad": YADFileChooser +} + + +class LinuxFileChooser(FileChooser): + '''FileChooser implementation for GNu/Linux. Accepts one additional + keyword argument, *desktop_override*, which, if set, overrides the + back-end that will be used. Set it to "gnome" for Zenity, to "kde" + for KDialog and to "yad" for YAD (Yet Another Dialog). + If set to None or not set, a default one will be picked based on + the running desktop environment and installed back-ends. + ''' + + desktop = None + if (str(os.environ.get("XDG_CURRENT_DESKTOP")).lower() == "kde" + and which("kdialog")): + desktop = "kde" + elif (str(os.environ.get("DESKTOP_SESSION")).lower() == "trinity" + and which('kdialog')): + desktop = "kde" + elif which("yad"): + desktop = "yad" + elif which("zenity"): + desktop = "gnome" + + def _file_selection_dialog(self, desktop_override=desktop, **kwargs): + if not desktop_override: + desktop_override = self.desktop + # This means we couldn't find any back-end + if not desktop_override: + raise OSError("No back-end available. Please install one.") + + chooser = CHOOSERS[desktop_override] + c = chooser(**kwargs) + return c.run() + + +def instance(): + return LinuxFileChooser() diff --git a/sbapp/plyer/platforms/linux/keystore.py b/sbapp/plyer/platforms/linux/keystore.py new file mode 100644 index 0000000..105ebd3 --- /dev/null +++ b/sbapp/plyer/platforms/linux/keystore.py @@ -0,0 +1,19 @@ +try: + import keyring +except ImportError: + raise NotImplementedError() + +from plyer.facades import Keystore + + +class LinuxKeystore(Keystore): + + def _set_key(self, servicename, key, value, **kwargs): + keyring.set_password(servicename, key, value) + + def _get_key(self, servicename, key, **kwargs): + return keyring.get_password(servicename, key) + + +def instance(): + return LinuxKeystore() diff --git a/sbapp/plyer/platforms/linux/notification.py b/sbapp/plyer/platforms/linux/notification.py new file mode 100644 index 0000000..95a6472 --- /dev/null +++ b/sbapp/plyer/platforms/linux/notification.py @@ -0,0 +1,108 @@ +''' +Module of Linux API for plyer.notification. +''' + +import warnings +import subprocess +from plyer.facades import Notification +from plyer.utils import whereis_exe +import os + + +class NotifyDesktopPortals(Notification): + ''' + Implementation of xdg-desktop-portals API. + ''' + + def _notify(self, **kwargs): + title = kwargs.get("title", "title") + body = kwargs.get("message", "body") + + subprocess.run([ + "gdbus", "call", "--session", "--dest", + "org.freedesktop.portal.Desktop", + "--object-path", "/org/freedesktop/portal/desktop", "--method", + "org.freedesktop.portal.Notification.AddNotification", "", + "{'title': <'" + title + "'>, 'body': <'" + body + "'>}" + ], stdout=subprocess.DEVNULL) + + +class NotifySendNotification(Notification): + ''' + Implementation of Linux notification API + using notify-send binary. + ''' + def _notify(self, **kwargs): + icon = kwargs.get('icon', '') + title = kwargs.get('title', 'title') + hint = kwargs.get('hint', 'string::') + message = kwargs.get('message', 'body') + category = kwargs.get('category', '') + app_name = kwargs.get('app_name', '') + urgency = kwargs.get('urgency', 'normal') + expire_time = kwargs.get('expire_time', '0') + + notify_send_args = (title, + message, + "-i", icon, + "-h", hint, + "-u", urgency, + "-c", category, + "-a", app_name, + "-t", expire_time) + + subprocess.call(["notify-send", *notify_send_args]) + + +class NotifyDbus(Notification): + ''' + Implementation of Linux notification API + using dbus library and dbus-python wrapper. + ''' + + def _notify(self, **kwargs): + summary = kwargs.get('title', "title") + body = kwargs.get('message', "body") + app_name = kwargs.get('app_name', '') + app_icon = kwargs.get('app_icon', '') + timeout = kwargs.get('timeout', 10) + actions = kwargs.get('actions', []) + hints = kwargs.get('hints', {}) + replaces_id = kwargs.get('replaces_id', 0) + + _bus_name = 'org.freedesktop.Notifications' + _object_path = '/org/freedesktop/Notifications' + _interface_name = _bus_name + + import dbus + session_bus = dbus.SessionBus() + obj = session_bus.get_object(_bus_name, _object_path) + interface = dbus.Interface(obj, _interface_name) + interface.Notify( + app_name, replaces_id, app_icon, + summary, body, actions, + hints, timeout * 1000 + ) + + +def instance(): + ''' + Instance for facade proxy. + ''' + if os.path.isdir("/app"): + # Flatpak + return NotifyDesktopPortals() + try: + import dbus # noqa: F401 + return NotifyDbus() + except ImportError: + msg = ("The Python dbus package is not installed.\n" + "Try installing it with your distribution's package manager, " + "it is usually called python-dbus or python3-dbus, but you " + "might have to try dbus-python instead, e.g. when using pip.") + warnings.warn(msg) + + if whereis_exe('notify-send'): + return NotifySendNotification() + warnings.warn("notify-send not found.") + return Notification() diff --git a/sbapp/plyer/platforms/linux/orientation.py b/sbapp/plyer/platforms/linux/orientation.py new file mode 100644 index 0000000..e60fa42 --- /dev/null +++ b/sbapp/plyer/platforms/linux/orientation.py @@ -0,0 +1,27 @@ +import subprocess as sb +from plyer.facades import Orientation + + +class LinuxOrientation(Orientation): + + def _set_landscape(self, **kwargs): + self.rotate = 'normal' + self.screen = sb.check_output( + "xrandr -q | grep ' connected' | head -n 1 | cut -d ' ' -f1", + shell=True + ) + self.screen = self.screen.decode('utf-8').split('\n')[0] + sb.call(["xrandr", "--output", self.screen, "--rotate", self.rotate]) + + def _set_portrait(self, **kwargs): + self.rotate = 'left' + self.screen = sb.check_output( + "xrandr -q | grep ' connected' | head -n 1 | cut -d ' ' -f1", + shell=True + ) + self.screen = self.screen.decode('utf-8').split('\n')[0] + sb.call(["xrandr", "--output", self.screen, "--rotate", self.rotate]) + + +def instance(): + return LinuxOrientation() diff --git a/sbapp/plyer/platforms/linux/processors.py b/sbapp/plyer/platforms/linux/processors.py new file mode 100644 index 0000000..74bb73a --- /dev/null +++ b/sbapp/plyer/platforms/linux/processors.py @@ -0,0 +1,37 @@ +from subprocess import Popen, PIPE +from plyer.facades import Processors +from plyer.utils import whereis_exe + +from os import environ + + +class LinuxProcessors(Processors): + def _get_state(self): + old_lang = environ.get('LANG') + environ['LANG'] = 'C' + + status = {"Number_of_Processors": None} + + dev = "--all" + nproc_process = Popen( + ["nproc", dev], + stdout=PIPE + ) + output = nproc_process.communicate()[0] + + environ['LANG'] = old_lang + + if not output: + return status + + status['Number_of_Processors'] = output.rstrip() + + return status + + +def instance(): + import sys + if whereis_exe('nproc'): + return LinuxProcessors() + sys.stderr.write("nproc not found.") + return Processors() diff --git a/sbapp/plyer/platforms/linux/screenshot.py b/sbapp/plyer/platforms/linux/screenshot.py new file mode 100644 index 0000000..00eeb5c --- /dev/null +++ b/sbapp/plyer/platforms/linux/screenshot.py @@ -0,0 +1,29 @@ +import subprocess +from os.path import join +from plyer.facades import Screenshot +from plyer.utils import whereis_exe +from plyer.platforms.linux.storagepath import LinuxStoragePath + + +class LinuxScreenshot(Screenshot): + def __init__(self, file_path=None): + default_path = join( + LinuxStoragePath().get_pictures_dir(), + 'screenshot.xwd' + ) + super().__init__(file_path or default_path) + + def _capture(self): + # call xwd and redirect bytes from stdout to file + with open(self.file_path, 'wb') as fle: + subprocess.call([ + # quiet, full screen root window + 'xwd', '-silent', '-root', + ], stdout=fle) + + +def instance(): + if whereis_exe('xwd'): + return LinuxScreenshot() + else: + return Screenshot() diff --git a/sbapp/plyer/platforms/linux/storagepath.py b/sbapp/plyer/platforms/linux/storagepath.py new file mode 100755 index 0000000..736674a --- /dev/null +++ b/sbapp/plyer/platforms/linux/storagepath.py @@ -0,0 +1,72 @@ +''' +Linux Storage Path +-------------------- +''' + +from plyer.facades import StoragePath +from os.path import expanduser, dirname, abspath, join, exists + +# Default paths for each name +USER_DIRS = "/.config/user-dirs.dirs" + +PATHS = { + "DESKTOP": "Desktop", + "DOCUMENTS": "Documents", + "DOWNLOAD": "Downloads", + "MUSIC": "Music", + "PICTURES": "Pictures", + "VIDEOS": "Videos" +} + + +class LinuxStoragePath(StoragePath): + + def _get_from_user_dirs(self, name): + home_dir = self._get_home_dir() + default = join(home_dir, PATHS[name]) + user_dirs = join(home_dir, USER_DIRS) + if not exists(user_dirs): + return default + + with open(user_dirs, "r") as f: + for line in f.readlines(): + if line.startswith("XDG_" + name): + return line.split('"')[1] + + return default + + def _get_home_dir(self): + return expanduser('~') + + def _get_external_storage_dir(self): + return "/media/" + self._get_home_dir().split("/")[-1] + + def _get_root_dir(self): + return "/" + + def _get_documents_dir(self): + directory = self._get_from_user_dirs("DOCUMENTS") + return directory.replace("$HOME", self._get_home_dir()) + + def _get_downloads_dir(self): + directory = self._get_from_user_dirs("DOWNLOAD") + return directory.replace("$HOME", self._get_home_dir()) + + def _get_videos_dir(self): + directory = self._get_from_user_dirs("VIDEOS") + return directory.replace("$HOME", self._get_home_dir()) + + def _get_music_dir(self): + directory = self._get_from_user_dirs("MUSIC") + return directory.replace("$HOME", self._get_home_dir()) + + def _get_pictures_dir(self): + directory = self._get_from_user_dirs("PICTURES") + return directory.replace("$HOME", self._get_home_dir()) + + def _get_application_dir(self): + return dirname(abspath(__name__)) + + +def instance(): + return LinuxStoragePath() diff --git a/sbapp/plyer/platforms/linux/tts.py b/sbapp/plyer/platforms/linux/tts.py new file mode 100644 index 0000000..3932772 --- /dev/null +++ b/sbapp/plyer/platforms/linux/tts.py @@ -0,0 +1,25 @@ +import subprocess +from plyer.facades import TTS +from plyer.utils import whereis_exe + + +class EspeakTextToSpeech(TTS): + ''' Speaks using the espeak program + ''' + def _speak(self, **kwargs): + subprocess.call(["espeak", kwargs.get('message')]) + + +class FliteTextToSpeech(TTS): + ''' Speaks using the flite program + ''' + def _speak(self, **kwargs): + subprocess.call(["flite", "-t", kwargs.get('message'), "play"]) + + +def instance(): + if whereis_exe('espeak'): + return EspeakTextToSpeech() + elif whereis_exe('flite'): + return FliteTextToSpeech() + return TTS() diff --git a/sbapp/plyer/platforms/linux/uniqueid.py b/sbapp/plyer/platforms/linux/uniqueid.py new file mode 100644 index 0000000..44926b1 --- /dev/null +++ b/sbapp/plyer/platforms/linux/uniqueid.py @@ -0,0 +1,47 @@ +''' +Module of Linux API for plyer.uniqueid. +''' + +from os import environ +from subprocess import Popen, PIPE +from plyer.facades import UniqueID +from plyer.utils import whereis_exe + + +class LinuxUniqueID(UniqueID): + ''' + Implementation of Linux uniqueid API. + ''' + + def _get_uid(self): + old_lang = environ.get('LANG') + environ['LANG'] = 'C' + stdout = Popen( + ["lshw", "-quiet"], + stdout=PIPE, stderr=PIPE + ).communicate()[0].decode('utf-8') + + output = u'' + for line in stdout.splitlines(): + if 'serial:' not in line: + continue + output = line + break + + environ['LANG'] = old_lang or u'' + result = None + + if output: + result = output.split()[1] + return result + + +def instance(): + ''' + Instance for facade proxy. + ''' + import sys + if whereis_exe('lshw'): + return LinuxUniqueID() + sys.stderr.write("lshw not found.") + return UniqueID() diff --git a/sbapp/plyer/platforms/linux/wifi.py b/sbapp/plyer/platforms/linux/wifi.py new file mode 100644 index 0000000..73f09b6 --- /dev/null +++ b/sbapp/plyer/platforms/linux/wifi.py @@ -0,0 +1,482 @@ +''' +.. note:: + This facade depends on `nmcli` (Network Manager command line tool). + It's found in most of the popular GNU/Linux distributions. Support for other + backends is not provided yet. +''' + +from subprocess import Popen, PIPE, call +from plyer.facades import Wifi +from plyer.utils import whereis_exe, deprecated + +try: + import wifi +except ModuleNotFoundError as err: + raise ModuleNotFoundError( + "python-wifi not installed. try:" + + "`pip install --user wifi`.") from err + + +class NMCLIWifi(Wifi): + ''' + .. versionadded:: 1.4.0 + ''' + + def __init__(self, *args, **kwargs): + ''' + .. versionadded:: 1.4.0 + ''' + + super().__init__(*args, **kwargs) + self.names = {} + + @property + def interfaces(self): + ''' + Get all the available interfaces for WiFi. + + .. versionadded:: 1.4.0 + Tested with nmcli 1.2.6. + ''' + if not self._is_enabled(): + self._enable() + + # fetch the devices + proc = Popen([ + 'nmcli', '--terse', + '--fields', 'DEVICE,TYPE', + 'device' + ], stdout=PIPE) + lines = proc.communicate()[0].decode('utf-8').splitlines() + + # filter devices by type + interfaces = [] + for line in lines: + # bad escape from nmcli's side :< + line = line.replace('\\:', '$$') + device, dtype = line.split(':') + if dtype != 'wifi': + continue + interfaces.append(device.replace('$$', ':')) + + # return wifi interfaces + return interfaces + + def _is_enabled(self): + ''' + Return the status of WiFi device. + + .. versionadded:: 1.4.0 + Tested with nmcli 1.2.6. + ''' + output = Popen( + ["nmcli", "radio", "wifi"], + stdout=PIPE + ).communicate()[0].decode('utf-8') + + if output.split()[0] == 'enabled': + return True + return False + + def _is_connected(self, interface=None): + ''' + Return whether a specified interface is connected to a WiFi network. + + .. versionadded:: 1.4.0 + Tested with nmcli 1.2.6. + ''' + if not self._is_enabled(): + self._enable() + if not interface: + interface = self.interfaces[0] + + # fetch all devices + proc = Popen([ + 'nmcli', '--terse', + '--fields', 'DEVICE,TYPE,STATE', + 'device' + ], stdout=PIPE) + lines = proc.communicate()[0].decode('utf-8').splitlines() + + # filter by wifi type and interface + connected = False + for line in lines: + line = line.replace('\\:', '$$') + device, dtype, state = line.split(':') + device = device.replace('$$', ':') + if dtype != 'wifi': + continue + + if device != interface: + continue + + if state == 'connected': + connected = True + + return connected + + def _start_scanning(self, interface=None): + ''' + Start scanning for available Wi-Fi networks + for the specified interface. + + .. versionadded:: 1.4.0 + Tested with nmcli 1.2.6. + ''' + if not self._is_enabled(): + self._enable() + if not interface: + interface = self.interfaces[0] + + # force rescan for fresh data + call(['nmcli', 'device', 'wifi', 'rescan', 'ifname', interface]) + + # get properties + fields = [ + 'SSID', 'BSSID', 'MODE', 'CHAN', 'FREQ', + 'BARS', 'RATE', 'SIGNAL', 'SECURITY' + ] + + # fetch all networks for interface + output = Popen([ + 'nmcli', '--terse', + '--fields', ','.join(fields), + 'device', 'wifi', 'list', 'ifname', interface + ], stdout=PIPE).communicate()[0].decode('utf-8') + + # parse output + for line in output.splitlines(): + line = line.replace('\\:', '$$') + row = { + field: value + for field, value in zip(fields, line.split(':')) + } + + row['BSSID'] = row['BSSID'].replace('$$', ':') + self.names[row['SSID']] = row + + def _get_network_info(self, name): + ''' + Get all the network information by network's name (SSID). + + .. versionadded:: 1.4.0 + Tested with nmcli 1.2.6. + ''' + if not self.names: + self._start_scanning() + + ret_list = {} + ret_list['ssid'] = self.names[name]['SSID'] + ret_list['signal'] = self.names[name]['SIGNAL'] + + bars = len(self.names[name]['BARS']) + ret_list['quality'] = '{}/100'.format(bars / 5.0 * 100) + ret_list['frequency'] = self.names[name]['FREQ'] + ret_list['bitrates'] = self.names[name]['RATE'] + + # wpa1, wpa2, wpa1 wpa2, wep, (none), perhaps something else + security = self.names[name]['SECURITY'].lower() + ret_list['encrypted'] = True + if 'wpa2' in security: + # wpa2, wpa2+wpa1 + ret_list['encryption_type'] = 'wpa2' + elif 'wpa' in security: + ret_list['encryption_type'] = 'wpa' + elif 'wep' in security: + ret_list['encryption_type'] = 'wep' + elif 'none' in security: + ret_list['encrypted'] = False + ret_list['encryption_type'] = 'none' + else: + ret_list['encryption_type'] = security + + ret_list['channel'] = int(self.names[name]['CHAN']) + ret_list['address'] = self.names[name]['BSSID'] + ret_list['mode'] = self.names[name]['MODE'] + return ret_list + + def _get_available_wifi(self): + ''' + Return the names of all found networks. + + .. versionadded:: 1.4.0 + Tested with nmcli 1.2.6. + ''' + if not self.names: + self._start_scanning() + return list(self.names.keys()) + + def _connect(self, network, parameters, interface=None): + ''' + Connect a specific interface to a WiFi network. + + Expects 2 parameters: + - SSID of the network + - parameters: dict + - password: string or None + + .. versionadded:: 1.4.0 + Tested with nmcli 1.2.6. + ''' + self._enable() + if not interface: + interface = self.interfaces[0] + + password = parameters.get('password') + command = [ + 'nmcli', 'device', 'wifi', 'connect', network, + 'ifname', interface + ] + if password: + command += ['password', password] + call(command) + + def _disconnect(self, interface=None): + ''' + Disconnect a specific interface from a WiFi network. + + .. versionadded:: 1.4.0 + Tested with nmcli 1.2.6. + ''' + if not self._is_enabled(): + return + + if not interface: + interface = self.interfaces[0] + + if self._nmcli_version() >= (1, 2, 6): + call(['nmcli', 'device', 'disconnect', interface]) + else: + call(['nmcli', 'nm', 'enable', 'false']) + + def _enable(self): + ''' + Turn WiFi device on. + + .. versionadded:: 1.4.0 + Tested with nmcli 1.2.6. + ''' + call(['nmcli', 'radio', 'wifi', 'on']) + + def _disable(self): + ''' + Turn WiFi device off. + + .. versionadded:: 1.4.0 + Tested with nmcli 1.2.6. + ''' + call(['nmcli', 'radio', 'wifi', 'off']) + + def _nmcli_version(self): + ''' + Get nmcli version to prevent executing deprecated commands. + + .. versionadded:: 1.4.0 + Tested with nmcli 1.2.6. + ''' + version = Popen(['nmcli', '-v'], stdout=PIPE) + version = version.communicate()[0].decode('utf-8') + while version and not version[0].isdigit(): + version = version[1:] + return tuple(map(int, (version.split('.')))) + + +@deprecated +class LinuxWifi(Wifi): + ''' + .. versionadded:: 1.2.5 + ''' + + def __init__(self, *args, **kwargs): + ''' + .. versionadded:: 1.4.0 + ''' + + super().__init__(*args, **kwargs) + self.names = {} + + @property + def interfaces(self): + ''' + .. versionadded:: 1.4.0 + ''' + + proc = Popen([ + 'nmcli', '--terse', + '--fields', 'DEVICE,TYPE', + 'device' + ], stdout=PIPE) + lines = proc.communicate()[0].decode('utf-8').splitlines() + + interfaces = [] + for line in lines: + device, dtype = line.split(':') + if dtype != 'wifi': + continue + interfaces.append(device) + + return interfaces + + def _is_enabled(self): + ''' + Returns `True` if wifi is enabled else `False`. + + .. versionadded:: 1.2.5 + .. versionchanged:: 1.3.2 + nmcli output is properly decoded to unicode + ''' + enbl = Popen(["nmcli", "radio", "wifi"], stdout=PIPE, stderr=PIPE) + if enbl.communicate()[0].split()[0].decode('utf-8') == "enabled": + return True + return False + + def _is_connected(self, interface=None): + ''' + .. versionadded:: 1.4.0 + ''' + + if not interface: + interface = self.interfaces[0] + + proc = Popen([ + 'nmcli', '--terse', + '--fields', 'DEVICE,TYPE,STATE', + 'device' + ], stdout=PIPE) + lines = proc.communicate()[0].decode('utf-8').splitlines() + + connected = False + for line in lines: + device, dtype, state = line.split(':') + if dtype != 'wifi': + continue + + if device != interface: + continue + + if state == 'connected': + connected = True + + return connected + + def _start_scanning(self, interface=None): + ''' + Returns all the network information. + + .. versionadded:: 1.2.5 + .. versionchanged:: 1.3.0 + scan only if wifi is enabled + ''' + + if not interface: + interface = self.interfaces[0] + + if self._is_enabled(): + list_ = list(wifi.Cell.all(interface)) + for i in range(len(list_)): + self.names[list_[i].ssid] = list_[i] + else: + raise Exception('Wifi not enabled.') + + def _get_network_info(self, name): + ''' + Starts scanning for available Wi-Fi networks and returns the available, + devices. + + .. versionadded:: 1.2.5 + ''' + ret_list = {} + ret_list['ssid'] = self.names[name].ssid + ret_list['signal'] = self.names[name].signal + ret_list['quality'] = self.names[name].quality + ret_list['frequency'] = self.names[name].frequency + ret_list['bitrates'] = self.names[name].bitrates + ret_list['encrypted'] = self.names[name].encrypted + ret_list['channel'] = self.names[name].channel + ret_list['address'] = self.names[name].address + ret_list['mode'] = self.names[name].mode + if not ret_list['encrypted']: + return ret_list + else: + ret_list['encryption_type'] = self.names[name].encryption_type + return ret_list + + def _get_available_wifi(self): + ''' + Returns the name of available networks. + + .. versionadded:: 1.2.5 + .. versionchanged:: 1.4.0 + return a proper list of elements instead of dict_keys + ''' + return list(self.names.keys()) + + def _connect(self, network, parameters, interface=None): + ''' + Expects 2 parameters: + - name/ssid of the network. + - parameters: dict type + - password: string or None + + .. versionadded:: 1.2.5 + ''' + if not interface: + interface = self.interfaces[0] + + result = None + try: + self._enable() + finally: + password = parameters['password'] + cell = self.names[network] + result = wifi.Scheme.for_cell( + interface, network, cell, password + ) + return result + + def _disconnect(self, interface=None): + ''' + Disconnect all the networks managed by Network manager. + + .. versionadded:: 1.2.5 + ''' + if not interface: + interface = self.interfaces[0] + + if self._nmcli_version() >= (1, 2, 6): + call(['nmcli', 'dev', 'disconnect', interface]) + else: + call(['nmcli', 'nm', 'enable', 'false']) + + def _enable(self): + ''' + Wifi interface power state is set to "ON". + + .. versionadded:: 1.3.2 + ''' + return call(['nmcli', 'radio', 'wifi', 'on']) + + def _disable(self): + ''' + Wifi interface power state is set to "OFF". + + .. versionadded:: 1.3.2 + ''' + return call(['nmcli', 'radio', 'wifi', 'off']) + + def _nmcli_version(self): + ''' + .. versionadded:: 1.3.2 + ''' + version = Popen(['nmcli', '-v'], stdout=PIPE) + version = version.communicate()[0].decode('utf-8') + while version and not version[0].isdigit(): + version = version[1:] + return tuple(map(int, (version.split('.')))) + + +def instance(): + if whereis_exe('nmcli'): + return NMCLIWifi() + + return LinuxWifi() diff --git a/sbapp/plyer/platforms/macosx/__init__.py b/sbapp/plyer/platforms/macosx/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sbapp/plyer/platforms/macosx/accelerometer.py b/sbapp/plyer/platforms/macosx/accelerometer.py new file mode 100644 index 0000000..91e72c3 --- /dev/null +++ b/sbapp/plyer/platforms/macosx/accelerometer.py @@ -0,0 +1,25 @@ +''' +MacOSX accelerometer +--------------------- +''' + +from plyer.facades import Accelerometer +from plyer.platforms.macosx.libs import osx_motion_sensor + + +class OSXAccelerometer(Accelerometer): + def _enable(self): + try: + osx_motion_sensor.get_coord() + except Exception: + raise Exception('Could not enable motion sensor on this macbook!') + + def _disable(self): + pass + + def _get_acceleration(self): + return osx_motion_sensor.get_coord() + + +def instance(): + return OSXAccelerometer() diff --git a/sbapp/plyer/platforms/macosx/audio.py b/sbapp/plyer/platforms/macosx/audio.py new file mode 100644 index 0000000..3ab9ce1 --- /dev/null +++ b/sbapp/plyer/platforms/macosx/audio.py @@ -0,0 +1,90 @@ +from os.path import join + +from pyobjus import autoclass +from pyobjus.dylib_manager import INCLUDE, load_framework + +from plyer.facades import Audio +from plyer.platforms.macosx.storagepath import OSXStoragePath + +load_framework(INCLUDE.Foundation) +load_framework(INCLUDE.AVFoundation) + +AVAudioPlayer = autoclass("AVAudioPlayer") +AVAudioRecorder = autoclass("AVAudioRecorder") +AVAudioFormat = autoclass("AVAudioFormat") +NSString = autoclass('NSString') +NSURL = autoclass('NSURL') +NSError = autoclass('NSError').alloc() + + +class OSXAudio(Audio): + def __init__(self, file_path=None): + default_path = join( + OSXStoragePath().get_music_dir(), + 'audio.wav' + ) + super().__init__(file_path or default_path) + + self._recorder = None + self._player = None + self._current_file = None + + def _start(self): + # Conversion of Python file path string to Objective-C NSString + file_path_NSString = NSString.alloc() + file_path_NSString = file_path_NSString.initWithUTF8String_( + self._file_path + ) + + # Definition of Objective-C NSURL object for the output record file + # specified by NSString file path + file_NSURL = NSURL.alloc() + file_NSURL = file_NSURL.initWithString_(file_path_NSString) + + # Internal audio file format specification + af = AVAudioFormat.alloc() + af = af.initWithCommonFormat_sampleRate_channels_interleaved_( + 1, 44100.0, 2, True + ) + + # Audio recorder instance initialization with specified file NSURL + # and audio file format + self._recorder = AVAudioRecorder.alloc() + self._recorder = self._recorder.initWithURL_format_error_( + file_NSURL, af, NSError + ) + + if not self._recorder: + raise Exception(NSError.code, NSError.domain) + + self._recorder.record() + + # Setting the currently recorded file as current file + # for using it as a parameter in audio player + self._current_file = file_NSURL + + def _stop(self): + if self._recorder: + self._recorder.stop() + self._recorder = None + + if self._player: + self._player.stop() + self._player = None + + def _play(self): + # Audio player instance initialization with the file NSURL + # of the last recorded audio file + self._player = AVAudioPlayer.alloc() + self._player = self._player.initWithContentsOfURL_error_( + self._current_file, NSError + ) + + if not self._player: + raise Exception(NSError.code, NSError.domain) + + self._player.play() + + +def instance(): + return OSXAudio() diff --git a/sbapp/plyer/platforms/macosx/battery.py b/sbapp/plyer/platforms/macosx/battery.py new file mode 100644 index 0000000..57965d3 --- /dev/null +++ b/sbapp/plyer/platforms/macosx/battery.py @@ -0,0 +1,59 @@ +''' +Module of MacOS API for plyer.battery. +''' + +from os import environ +from subprocess import Popen, PIPE +from plyer.facades import Battery +from plyer.utils import whereis_exe + + +class OSXBattery(Battery): + ''' + Implementation of MacOS battery API. + ''' + + def _get_state(self): + old_lang = environ.get('LANG', '') + environ['LANG'] = 'C' + + status = {"isCharging": None, "percentage": None} + + ioreg_process = Popen( + ["ioreg", "-rc", "AppleSmartBattery"], + stdout=PIPE + ) + output = ioreg_process.communicate()[0] + + environ['LANG'] = old_lang + + if not output: + return status + + is_charging = max_capacity = current_capacity = None + for line in output.decode('utf-8').splitlines(): + if 'IsCharging' in line: + is_charging = line.rpartition('=')[-1].strip() + if 'MaxCapacity' in line: + max_capacity = float(line.rpartition('=')[-1].strip()) + if 'CurrentCapacity' in line: + current_capacity = float(line.rpartition('=')[-1].strip()) + + if is_charging: + status['isCharging'] = is_charging == "Yes" + + if current_capacity and max_capacity: + status['percentage'] = 100.0 * current_capacity / max_capacity + + return status + + +def instance(): + ''' + Instance for facade proxy. + ''' + import sys + if whereis_exe('ioreg'): + return OSXBattery() + sys.stderr.write("ioreg not found.") + return Battery() diff --git a/sbapp/plyer/platforms/macosx/bluetooth.py b/sbapp/plyer/platforms/macosx/bluetooth.py new file mode 100644 index 0000000..2575817 --- /dev/null +++ b/sbapp/plyer/platforms/macosx/bluetooth.py @@ -0,0 +1,54 @@ +''' +Module of MacOS API for plyer.bluetooth. +''' + +from subprocess import Popen, PIPE +from plyer.facades import Bluetooth +from plyer.utils import whereis_exe + +from os import environ + + +class OSXBluetooth(Bluetooth): + ''' + Implementation of MacOS bluetooth API. + ''' + + def _get_info(self): + old_lang = environ.get('LANG') + environ['LANG'] = 'C' + + sys_profiler_process = Popen( + ["system_profiler", "SPBluetoothDataType"], + stdout=PIPE + ) + + stdout = sys_profiler_process.communicate()[0].decode('utf-8') + output = stdout.splitlines() + + lines = [] + for line in output: + if 'Bluetooth Power' not in line: + continue + lines.append(line) + + if old_lang is None: + environ.pop('LANG') + else: + environ['LANG'] = old_lang + + if output and len(lines) == 1: + return lines[0].split()[2] + else: + return None + + +def instance(): + ''' + Instance for facade proxy. + ''' + import sys + if whereis_exe('system_profiler'): + return OSXBluetooth() + sys.stderr.write("system_profiler not found.") + return Bluetooth() diff --git a/sbapp/plyer/platforms/macosx/cpu.py b/sbapp/plyer/platforms/macosx/cpu.py new file mode 100644 index 0000000..7b7da7f --- /dev/null +++ b/sbapp/plyer/platforms/macosx/cpu.py @@ -0,0 +1,62 @@ +''' +Module of MacOS API for plyer.cpu. +''' + +from subprocess import Popen, PIPE +from plyer.facades import CPU +from plyer.utils import whereis_exe + + +class OSXCPU(CPU): + ''' + Implementation of MacOS CPU API. + ''' + + @staticmethod + def _sockets(): + return + + def _physical(self): + # cores + physical = None + + _physical = Popen( + ['sysctl', '-n', 'hw.physicalcpu_max'], + stdout=PIPE + ) + output = _physical.communicate()[0].decode('utf-8').strip() + if output: + physical = int(output) + return physical + + def _logical(self): + # cores * threads + logical = None + + _logical = Popen( + ['sysctl', '-n', 'hw.logicalcpu_max'], + stdout=PIPE + ) + output = _logical.communicate()[0].decode('utf-8').strip() + if output: + logical = int(output) + return logical + + @staticmethod + def _cache(): + return + + @staticmethod + def _numa(): + return + + +def instance(): + ''' + Instance for facade proxy. + ''' + import sys + if whereis_exe('sysctl'): + return OSXCPU() + sys.stderr.write('sysctl not found.') + return CPU() diff --git a/sbapp/plyer/platforms/macosx/devicename.py b/sbapp/plyer/platforms/macosx/devicename.py new file mode 100644 index 0000000..6652425 --- /dev/null +++ b/sbapp/plyer/platforms/macosx/devicename.py @@ -0,0 +1,23 @@ +''' +Module of MacOSX API for plyer.devicename. +''' + +import socket +from plyer.facades import DeviceName + + +class OSXDeviceName(DeviceName): + ''' + Implementation of MacOSX DeviceName API. + ''' + + def _get_device_name(self): + hostname = socket.gethostname() + return hostname + + +def instance(): + ''' + Instance for facade proxy. + ''' + return OSXDeviceName() diff --git a/sbapp/plyer/platforms/macosx/email.py b/sbapp/plyer/platforms/macosx/email.py new file mode 100644 index 0000000..49c5d4d --- /dev/null +++ b/sbapp/plyer/platforms/macosx/email.py @@ -0,0 +1,49 @@ +''' +Module of MacOS API for plyer.email. +''' + +import subprocess + +try: + from urllib.parse import quote +except ImportError: + from urllib import quote + +from plyer.facades import Email +from plyer.utils import whereis_exe + + +class MacOSXEmail(Email): + ''' + Implementation of MacOS email API. + ''' + + def _send(self, **kwargs): + recipient = kwargs.get('recipient') + subject = kwargs.get('subject') + text = kwargs.get('text') + + uri = "mailto:" + if recipient: + uri += str(recipient) + if subject: + uri += "?" if "?" not in uri else "&" + uri += "subject=" + uri += quote(str(subject)) + if text: + uri += "?" if "?" not in uri else "&" + uri += "body=" + uri += quote(str(text)) + + subprocess.Popen(["open", uri]) + + +def instance(): + ''' + Instance for facade proxy. + ''' + import sys + if whereis_exe('open'): + return MacOSXEmail() + sys.stderr.write("open not found.") + return Email() diff --git a/sbapp/plyer/platforms/macosx/filechooser.py b/sbapp/plyer/platforms/macosx/filechooser.py new file mode 100644 index 0000000..85fb91c --- /dev/null +++ b/sbapp/plyer/platforms/macosx/filechooser.py @@ -0,0 +1,126 @@ +''' +Mac OS X file chooser +--------------------- +''' + +from plyer.facades import FileChooser +from pyobjus import autoclass, objc_arr, objc_str +from pyobjus.dylib_manager import load_framework, INCLUDE + +load_framework(INCLUDE.AppKit) +NSURL = autoclass('NSURL') +NSOpenPanel = autoclass('NSOpenPanel') +NSSavePanel = autoclass('NSSavePanel') +NSOKButton = 1 + + +class MacFileChooser: + '''A native implementation of file chooser dialogs using Apple's API + through pyobjus. + + Not implemented features: + * filters (partial, wildcards are converted to extensions if possible. + Pass the Mac-specific "use_extensions" if you can provide + Mac OS X-compatible to avoid automatic conversion) + * multiple (only for save dialog. Available in open dialog) + * icon + * preview + ''' + + mode = "open" + path = None + multiple = False + filters = [] + preview = False + title = None + icon = None + show_hidden = False + use_extensions = False + + def __init__(self, *args, **kwargs): + self._handle_selection = kwargs.pop( + 'on_selection', self._handle_selection + ) + + # Simulate Kivy's behavior + for i in kwargs: + setattr(self, i, kwargs[i]) + + @staticmethod + def _handle_selection(selection): + ''' + Dummy placeholder for returning selection from chooser. + ''' + return selection + + def run(self): + panel = None + if self.mode in ("open", "dir", "dir_and_files"): + panel = NSOpenPanel.openPanel() + + panel.setCanChooseDirectories_(self.mode != "open") + panel.setCanChooseFiles_(self.mode != "dir") + + if self.multiple: + panel.setAllowsMultipleSelection_(True) + elif self.mode == "save": + panel = NSSavePanel.savePanel() + else: + assert False, self.mode + + panel.setCanCreateDirectories_(True) + panel.setShowsHiddenFiles_(self.show_hidden) + + if self.title: + panel.setTitle_(objc_str(self.title)) + + # Mac OS X does not support wildcards unlike the other platforms. + # This tries to convert wildcards to "extensions" when possible, + # ans sets the panel to also allow other file types, just to be safe. + if self.filters: + filthies = [] + for f in self.filters: + if type(f) == str: + f = (None, f) + for s in f[1:]: + if not self.use_extensions: + if s.strip().endswith("*"): + continue + pystr = s.strip().split("*")[-1].split(".")[-1] + filthies.append(objc_str(pystr)) + + ftypes_arr = objc_arr(*filthies) + # todo: switch to allowedContentTypes + panel.setAllowedFileTypes_(ftypes_arr) + panel.setAllowsOtherFileTypes_(not self.use_extensions) + + if self.path: + url = NSURL.fileURLWithPath_(self.path) + panel.setDirectoryURL_(url) + + selection = None + + if panel.runModal(): + if self.mode == "save" or not self.multiple: + selection = [panel.filename().UTF8String()] + else: + filename = panel.filenames() + selection = [ + filename.objectAtIndex_(x).UTF8String() + for x in range(filename.count())] + + self._handle_selection(selection) + + return selection + + +class MacOSXFileChooser(FileChooser): + ''' + FileChooser implementation for macOS using NSOpenPanel, NSSavePanel. + ''' + def _file_selection_dialog(self, **kwargs): + return MacFileChooser(**kwargs).run() + + +def instance(): + return MacOSXFileChooser() diff --git a/sbapp/plyer/platforms/macosx/keystore.py b/sbapp/plyer/platforms/macosx/keystore.py new file mode 100644 index 0000000..11e60e8 --- /dev/null +++ b/sbapp/plyer/platforms/macosx/keystore.py @@ -0,0 +1,19 @@ +try: + import keyring +except ImportError: + raise NotImplementedError() + +from plyer.facades import Keystore + + +class OSXKeystore(Keystore): + + def _set_key(self, servicename, key, value, **kwargs): + keyring.set_password(servicename, key, value) + + def _get_key(self, servicename, key, **kwargs): + return keyring.get_password(servicename, key) + + +def instance(): + return OSXKeystore() diff --git a/sbapp/plyer/platforms/macosx/libs/__init__.py b/sbapp/plyer/platforms/macosx/libs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sbapp/plyer/platforms/macosx/libs/osx_motion_sensor.py b/sbapp/plyer/platforms/macosx/libs/osx_motion_sensor.py new file mode 100644 index 0000000..d49df43 --- /dev/null +++ b/sbapp/plyer/platforms/macosx/libs/osx_motion_sensor.py @@ -0,0 +1,128 @@ +import ctypes +from ctypes import ( + Structure, cdll, sizeof, + c_int8, c_int16, c_size_t +) +from ctypes.util import find_library +import platform + +ERROR_DICT = { + "0": "IOKit Framework not found, is this OSX?", + "-1": "No SMCMotionSensor service", + "-2": "No sms device", + "-3": "Could not open motion sensor device", + "-4": "Did not receive any coordinates" +} + +IOKit = cdll.LoadLibrary(find_library('IOKit')) + + +class data_structure(Structure): + _fields_ = [ + ('x', c_int16), + ('y', c_int16), + ('z', c_int16), + ('pad', c_int8 * 34), + ] + + +void_p = ctypes.POINTER(ctypes.c_int) + +kern_return_t = ctypes.c_int +KERN_SUCCESS = 0 +KERN_FUNC = 5 # SMC Motion Sensor on MacBook Pro + +mach_port_t = void_p +MACH_PORT_NULL = 0 + +io_object_t = ctypes.c_int +io_object_t = ctypes.c_int +io_iterator_t = void_p +io_object_t = void_p +io_connect_t = void_p +IOItemCount = ctypes.c_uint + +CFMutableDictionaryRef = void_p + + +def is_os_64bit(): + return platform.machine().endswith('64') + + +def read_sms(): + result = kern_return_t() + masterPort = mach_port_t() + + result = IOKit.IOMasterPort(MACH_PORT_NULL, ctypes.byref(masterPort)) + + IOKit.IOServiceMatching.restype = CFMutableDictionaryRef + matchingDictionary = IOKit.IOServiceMatching("SMCMotionSensor") + + iterator = io_iterator_t() + result = IOKit.IOServiceGetMatchingServices( + masterPort, matchingDictionary, + ctypes.byref(iterator) + ) + + if (result != KERN_SUCCESS): + raise ("No coordinates received!") + return -1, None + + IOKit.IOIteratorNext.restype = io_object_t + smsDevice = IOKit.IOIteratorNext(iterator) + + if not smsDevice: + return -2, None + + dataPort = io_connect_t() + result = IOKit.IOServiceOpen( + smsDevice, IOKit.mach_task_self(), + 0, ctypes.byref(dataPort) + ) + + if (result != KERN_SUCCESS): + return -3, None + + inStructure = data_structure() + outStructure = data_structure() + + if(is_os_64bit() or hasattr(IOKit, 'IOConnectCallStructMethod')): + structureInSize = IOItemCount(sizeof(data_structure)) + structureOutSize = c_size_t(sizeof(data_structure)) + + result = IOKit.IOConnectCallStructMethod( + dataPort, KERN_FUNC, + ctypes.byref(inStructure), structureInSize, + ctypes.byref(outStructure), ctypes.byref(structureOutSize) + ) + else: + structureInSize = IOItemCount(sizeof(data_structure)) + structureOutSize = IOItemCount(sizeof(data_structure)) + + result = IOKit.IOConnectMethodStructureIStructureO( + dataPort, KERN_FUNC, + structureInSize, ctypes.byref(structureOutSize), + ctypes.byref(inStructure), ctypes.byref(outStructure) + ) + + IOKit.IOServiceClose(dataPort) + + if (result != KERN_SUCCESS): + return -4, None + + return 1, outStructure + + +def get_coord(): + if not IOKit: + raise Exception(ERROR_DICT["0"]) + + ret, data = read_sms() + + if (ret > 0): + if(data.x): + return (data.x, data.y, data.z) + else: + return (None, None, None) + else: + raise Exception(ERROR_DICT[str(ret)]) diff --git a/sbapp/plyer/platforms/macosx/libs/osx_paths.py b/sbapp/plyer/platforms/macosx/libs/osx_paths.py new file mode 100644 index 0000000..9b26a78 --- /dev/null +++ b/sbapp/plyer/platforms/macosx/libs/osx_paths.py @@ -0,0 +1,21 @@ +import ctypes +import os + + +def NSIterateSearchPaths(directory): + LibraryPath = ("/System/Library/Frameworks/CoreFoundation.framework/" + "Versions/A/CoreFoundation") + CoreFound = ctypes.cdll.LoadLibrary(LibraryPath) + NSStartSearchPathEnumeration = CoreFound.NSStartSearchPathEnumeration + NSGetNextSearchPathEnumeration = CoreFound.NSGetNextSearchPathEnumeration + PATH_MAX = os.pathconf('/', os.pathconf_names['PC_PATH_MAX']) + PATH_ENCODING = 'utf8' + path_buffer = ctypes.create_string_buffer(PATH_MAX) + # paths = [] <- fixme, possible list of paths in directory + state = NSStartSearchPathEnumeration(directory, 1) + while True: + state = NSGetNextSearchPathEnumeration(state, path_buffer) + if state == 0: + break + path = os.path.expanduser(path_buffer.value.decode(PATH_ENCODING)) + return path diff --git a/sbapp/plyer/platforms/macosx/notification.py b/sbapp/plyer/platforms/macosx/notification.py new file mode 100644 index 0000000..ed78ab1 --- /dev/null +++ b/sbapp/plyer/platforms/macosx/notification.py @@ -0,0 +1,51 @@ +''' +Module of MacOS API for plyer.notification. +''' + +from plyer.facades import Notification + +from pyobjus import ( + autoclass, protocol, objc_str, ObjcBOOL +) +from pyobjus.dylib_manager import ( + load_framework, INCLUDE +) + +load_framework(INCLUDE.AppKit) +load_framework(INCLUDE.Foundation) + +NSUserNotification = autoclass('NSUserNotification') +NSUserNotificationCenter = autoclass('NSUserNotificationCenter') + + +class OSXNotification(Notification): + ''' + Implementation of MacOS notification API. + ''' + + def _notify(self, **kwargs): + title = kwargs.get('title', '') + message = kwargs.get('message', '') + app_name = kwargs.get('app_name', '') + # app_icon, timeout, ticker are not supported (yet) + + notification = NSUserNotification.alloc().init() + notification.setTitle_(objc_str(title)) + notification.setSubtitle_(objc_str(app_name)) + notification.setInformativeText_(objc_str(message)) + + usrnotifctr = NSUserNotificationCenter.defaultUserNotificationCenter() + usrnotifctr.setDelegate_(self) + usrnotifctr.deliverNotification_(notification) + + @protocol('NSUserNotificationCenterDelegate') + def userNotificationCenter_shouldPresentNotification_( + self, center, notification): + return ObjcBOOL(True) + + +def instance(): + ''' + Instance for facade proxy. + ''' + return OSXNotification() diff --git a/sbapp/plyer/platforms/macosx/screenshot.py b/sbapp/plyer/platforms/macosx/screenshot.py new file mode 100644 index 0000000..c76766a --- /dev/null +++ b/sbapp/plyer/platforms/macosx/screenshot.py @@ -0,0 +1,27 @@ +import subprocess +from os.path import join +from plyer.facades import Screenshot +from plyer.utils import whereis_exe +from plyer.platforms.macosx.storagepath import OSXStoragePath + + +class OSXScreenshot(Screenshot): + def __init__(self, file_path=None): + default_path = join( + OSXStoragePath().get_pictures_dir().replace('file://', ''), + 'screenshot.png' + ) + super().__init__(file_path or default_path) + + def _capture(self): + subprocess.call([ + 'screencapture', + self.file_path + ]) + + +def instance(): + if whereis_exe('screencapture'): + return OSXScreenshot() + else: + return Screenshot() diff --git a/sbapp/plyer/platforms/macosx/storagepath.py b/sbapp/plyer/platforms/macosx/storagepath.py new file mode 100644 index 0000000..027b04e --- /dev/null +++ b/sbapp/plyer/platforms/macosx/storagepath.py @@ -0,0 +1,62 @@ +''' +MacOS X Storage Path +-------------------- +''' + +from plyer.facades import StoragePath +from pyobjus import autoclass + +NSFileManager = autoclass('NSFileManager') + +# Directory constants (NSSearchPathDirectory enumeration) +NSApplicationDirectory = 1 +NSDocumentDirectory = 9 +NSDownloadsDirectory = 15 +NSMoviesDirectory = 17 +NSMusicDirectory = 18 +NSPicturesDirectory = 19 + + +class OSXStoragePath(StoragePath): + + def __init__(self): + self.defaultManager = NSFileManager.defaultManager() + + def _get_home_dir(self): + home_dir_NSURL = self.defaultManager.homeDirectoryForCurrentUser + return home_dir_NSURL.absoluteString.UTF8String() + + def _get_external_storage_dir(self): + return 'Method not implemented for current platform.' + + def _get_root_dir(self): + return '/' + + def _get_documents_dir(self): + return self.defaultManager.URLsForDirectory_inDomains_( + NSDocumentDirectory, 1).firstObject().absoluteString.UTF8String() + + def _get_downloads_dir(self): + return self.defaultManager.URLsForDirectory_inDomains_( + NSDownloadsDirectory, 1).firstObject().absoluteString.UTF8String() + + def _get_videos_dir(self): + return self.defaultManager.URLsForDirectory_inDomains_( + NSMoviesDirectory, 1).firstObject().absoluteString.UTF8String() + + def _get_music_dir(self): + return self.defaultManager.URLsForDirectory_inDomains_( + NSMusicDirectory, 1).firstObject().absoluteString.UTF8String() + + def _get_pictures_dir(self): + return self.defaultManager.URLsForDirectory_inDomains_( + NSPicturesDirectory, 1).firstObject().absoluteString.UTF8String() + + def _get_application_dir(self): + return self.defaultManager.URLsForDirectory_inDomains_( + NSApplicationDirectory, 1 + ).firstObject().absoluteString.UTF8String() + + +def instance(): + return OSXStoragePath() diff --git a/sbapp/plyer/platforms/macosx/tts.py b/sbapp/plyer/platforms/macosx/tts.py new file mode 100644 index 0000000..755e820 --- /dev/null +++ b/sbapp/plyer/platforms/macosx/tts.py @@ -0,0 +1,25 @@ +import subprocess +from plyer.facades import TTS +from plyer.utils import whereis_exe + + +class NativeSayTextToSpeech(TTS): + '''Speaks using the native OSX 'say' command + ''' + def _speak(self, **kwargs): + subprocess.call(["say", kwargs.get('message')]) + + +class EspeakTextToSpeech(TTS): + '''Speaks using the espeak program + ''' + def _speak(self, **kwargs): + subprocess.call(["espeak", kwargs.get('message')]) + + +def instance(): + if whereis_exe('say'): + return NativeSayTextToSpeech() + elif whereis_exe('espeak'): + return EspeakTextToSpeech() + return TTS() diff --git a/sbapp/plyer/platforms/macosx/uniqueid.py b/sbapp/plyer/platforms/macosx/uniqueid.py new file mode 100644 index 0000000..dc84153 --- /dev/null +++ b/sbapp/plyer/platforms/macosx/uniqueid.py @@ -0,0 +1,47 @@ +''' +Module of MacOS API for plyer.uniqueid. +''' + +from os import environ +from subprocess import Popen, PIPE +from plyer.facades import UniqueID +from plyer.utils import whereis_exe + + +class OSXUniqueID(UniqueID): + ''' + Implementation of MacOS uniqueid API. + ''' + + def _get_uid(self): + old_lang = environ.get('LANG') + environ['LANG'] = 'C' + + ioreg_process = Popen(["ioreg", "-l"], stdout=PIPE) + grep_process = Popen( + ["grep", "IOPlatformSerialNumber"], + stdin=ioreg_process.stdout, stdout=PIPE + ) + ioreg_process.stdout.close() + output = grep_process.communicate()[0] + + if old_lang is None: + environ.pop('LANG') + else: + environ['LANG'] = old_lang + + result = None + if output: + result = output.split()[3][1:-1] + return result + + +def instance(): + ''' + Instance for facade proxy. + ''' + import sys + if whereis_exe('ioreg'): + return OSXUniqueID() + sys.stderr.write("ioreg not found.") + return UniqueID() diff --git a/sbapp/plyer/platforms/macosx/wifi.py b/sbapp/plyer/platforms/macosx/wifi.py new file mode 100644 index 0000000..32c02ab --- /dev/null +++ b/sbapp/plyer/platforms/macosx/wifi.py @@ -0,0 +1,147 @@ +from pyobjus import autoclass +from pyobjus.dylib_manager import load_framework, INCLUDE + +from plyer.facades import Wifi + +load_framework(INCLUDE.Foundation) +load_framework(INCLUDE.CoreWLAN) + +CWInterface = autoclass('CWInterface') +CWNetwork = autoclass('CWNetwork') +CWWiFiClient = autoclass('CWWiFiClient') +NSArray = autoclass('NSArray') +NSDictionary = autoclass('NSDictionary') +NSString = autoclass('NSString') + + +class OSXWifi(Wifi): + names = {} + + def _is_enabled(self): + ''' + Returns `True` if the Wifi is enabled else returns `False`. + ''' + return CWWiFiClient.sharedWiFiClient().interface().powerOn() + + def _get_network_info(self, name): + ''' + Returns all the network information. + ''' + + accessNetworkType = self.names[name].accessNetworkType + aggregateRSSI = self.names[name].aggregateRSSI + beaconInterval = self.names[name].beaconInterval + bssid = self.names[name].bssid.UTF8String() + countryCode = self.names[name].countryCode + hasInternet = self.names[name].hasInternet + hasInterworkingIE = self.names[name].hasInterworkingIE + hessid = self.names[name].hessid + ibss = self.names[name].ibss + isAdditionalStepRequiredForAccess = \ + self.names[name].isAdditionalStepRequiredForAccess + isCarPlayNetwork = self.names[name].isCarPlayNetwork + isEmergencyServicesReachable = \ + self.names[name].isEmergencyServicesReachable + isPasspoint = self.names[name].isPasspoint + isPersonalHotspot = self.names[name].isPersonalHotspot + isUnauthenticatedEmergencyServiceAccessible = \ + self.names[name].isUnauthenticatedEmergencyServiceAccessible + noiseMeasurement = self.names[name].noiseMeasurement + physicalLayerMode = self.names[name].physicalLayerMode + rssiValue = self.names[name].rssiValue + securityType = self.names[name].securityType + ssid = self.names[name].ssid.UTF8String() + supportsEasyConnect = self.names[name].supportsEasyConnect + supportsWPS = self.names[name].supportsWPS + venueGroup = self.names[name].venueGroup + venueType = self.names[name].venueType + + return {'accessNetworkType': accessNetworkType, + 'aggregateRSSI': aggregateRSSI, + 'beaconInterval': beaconInterval, + 'bssid': bssid, + 'countryCode': countryCode, + 'hasInternet': hasInternet, + 'hasInterworkingIE': hasInterworkingIE, + 'hessid': hessid, + 'ibss': ibss, + 'isAdditionalStepRequiredForAccess': + isAdditionalStepRequiredForAccess, + 'isCarPlayNetwork': isCarPlayNetwork, + 'isEmergencyServicesReachable': isEmergencyServicesReachable, + 'isPasspoint': isPasspoint, + 'isPersonalHotspot': isPersonalHotspot, + 'isUnauthenticatedEmergencyServiceAccessible': + isUnauthenticatedEmergencyServiceAccessible, + 'noiseMeasurement': noiseMeasurement, + 'physicalLayerMode': physicalLayerMode, + 'rssiValue': rssiValue, + 'securityType': securityType, + 'ssid': ssid, + 'supportsEasyConnect': supportsEasyConnect, + 'supportsWPS': supportsWPS, + 'venueGroup': venueGroup, + 'venueType': venueType} + + def _start_scanning(self): + ''' + Starts scanning for available Wi-Fi networks. + ''' + if self._is_enabled(): + self.names = {} + c = CWInterface.interface() + scan = c.scanForNetworksWithName_error_(None, None) + cnt = scan.allObjects().count() + for i in range(cnt): + self.names[ + scan.allObjects().objectAtIndex_(i).ssid.UTF8String() + ] = scan.allObjects().objectAtIndex_(i) + else: + raise Exception("Wifi not enabled.") + + def _get_available_wifi(self): + ''' + Returns the name of available networks. + ''' + return self.names.keys() + + def _connect(self, network, parameters): + ''' + Expects 2 parameters: + - name/ssid of the network. + - password: dict type + ''' + password = parameters['password'] + network_object = self.names[network] + CWInterface.interface().associateToNetwork_password_error_( + network_object, + password, + None) + return + + def _disconnect(self): + ''' + Disconnect from network. + ''' + CWInterface.interface().disassociate() + return + + def _disable(self): + ''' + Wifi interface power state is set to "OFF". + ''' + + interface = CWWiFiClient.sharedWiFiClient().interface() + interface.setPower_error_(False, None) + + def _enable(self): + ''' + Wifi interface power state is set to "ON". + ''' + + interface = CWWiFiClient.sharedWiFiClient().interface() + interface.setPower_error_(True, None) + + +def instance(): + return OSXWifi() diff --git a/sbapp/plyer/platforms/win/__init__.py b/sbapp/plyer/platforms/win/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sbapp/plyer/platforms/win/audio.py b/sbapp/plyer/platforms/win/audio.py new file mode 100644 index 0000000..c9d8f35 --- /dev/null +++ b/sbapp/plyer/platforms/win/audio.py @@ -0,0 +1,398 @@ +''' +Documentation: +http://docs.microsoft.com/en-us/windows/desktop/Multimedia + +.. versionadded:: 1.4.0 +''' + +from os.path import join + +from ctypes import windll +from ctypes import ( + sizeof, c_void_p, c_ulonglong, c_ulong, + c_wchar_p, byref, Structure, create_string_buffer +) +from ctypes.wintypes import DWORD, UINT + +from plyer.facades import Audio +from plyer.platforms.win.storagepath import WinStoragePath + +# DWORD_PTR i.e. ULONG_PTR, 32/64bit +ULONG_PTR = c_ulonglong if sizeof(c_void_p) == 8 else c_ulong + +# device specific symbols +MCI_OPEN = 0x803 +MCI_OPEN_TYPE = 0x2000 +MCI_OPEN_ELEMENT = 512 +MCI_RECORD = 0x80F +MCI_STOP = 0x808 +MCI_SAVE = 0x813 +MCI_PLAY = 0x806 +MCI_CLOSE = 0x804 + +# recorder specific symbols +MCI_FROM = 4 +MCI_TO = 8 +MCI_WAIT = 2 +MCI_SAVE_FILE = 256 + + +class MCI_OPEN_PARMS(Structure): + ''' + Struct for MCI_OPEN message parameters. + + .. versionadded:: 1.4.0 + ''' + + _fields_ = [ + ('mciOpenParms', ULONG_PTR), + ('wDeviceID', UINT), + ('lpstrDeviceType', c_wchar_p), + ('lpstrElementName', c_wchar_p), + ('lpstrAlias', c_wchar_p) + ] + + +class MCI_RECORD_PARMS(Structure): + ''' + Struct for MCI_RECORD message parameters. + + http://docs.microsoft.com/en-us/windows/desktop/Multimedia/mci-record-parms + + .. versionadded:: 1.4.0 + ''' + + _fields_ = [ + ('dwCallback', ULONG_PTR), + ('dwFrom', DWORD), + ('dwTo', DWORD) + ] + + +class MCI_SAVE_PARMS(Structure): + ''' + Struct for MCI_SAVE message parameters. + + http://docs.microsoft.com/en-us/windows/desktop/Multimedia/mci-save-parms + + .. versionadded:: 1.4.0 + ''' + + _fields_ = [ + ('dwCallback', ULONG_PTR), + ('lpfilename', c_wchar_p) + ] + + +class MCI_PLAY_PARMS(Structure): + ''' + Struct for MCI_PLAY message parameters. + + http://docs.microsoft.com/en-us/windows/desktop/Multimedia/mci-play-parms + + .. versionadded:: 1.4.0 + ''' + + _fields_ = [ + ('dwCallback', ULONG_PTR), + ('dwFrom', DWORD), + ('dwTo', DWORD) + ] + + +def send_command(device, msg, flags, params): + ''' + Generic mciSendCommandW() wrapper with error handler. + All parameters are required as for mciSendCommandW(). + In case of no `params` passed, use `None`, that value + won't be dereferenced. + + .. versionadded:: 1.4.0 + ''' + + multimedia = windll.winmm + send_command_w = multimedia.mciSendCommandW + get_error = multimedia.mciGetErrorStringW + + # error text buffer + # by API specification 128 is max, however the API sometimes + # kind of does not respect the documented bounds and returns + # more characters than buffer length...?! + error_len = 128 + + # big enough to prevent API accidentally segfaulting + error_text = create_string_buffer(error_len * 2) + + # open a recording device with a new file + error_code = send_command_w( + device, # device ID + msg, + flags, + + # reference to parameters structure or original value + # in case of params=False/0/None/... + byref(params) if params else params + ) + + # handle error messages if any + if error_code: + # device did not open, raise an exception + get_error(error_code, byref(error_text), error_len) + error_text = error_text.raw.replace(b'\x00', b'').decode('utf-8') + + # either it can close already open device or it will fail because + # the device is in non-closable state, but the end result is the same + # and it makes no sense to parse MCI_CLOSE's error in this case + send_command_w(device, MCI_CLOSE, 0, None) + raise Exception(error_code, error_text) + + # return params struct because some commands write into it + # to pass some values out of the local function scope + return params + + +class WinRecorder: + ''' + Generic wrapper for MCI_RECORD handling the filenames and device closing + in the same approach like it is used for other platforms. + + .. versionadded:: 1.4.0 + ''' + + def __init__(self, device, filename): + self._device = device + self._filename = filename + + @property + def device(self): + ''' + Public property returning device ID. + + .. versionadded:: 1.4.0 + ''' + return self._device + + @property + def filename(self): + ''' + Public property returning filename for current recording. + + .. versionadded:: 1.4.0 + ''' + return self._filename + + def record(self): + ''' + Start recording a WAV sound. + + .. versionadded:: 1.4.0 + ''' + send_command( + device=self.device, + msg=MCI_RECORD, + flags=0, + params=None + ) + + def stop(self): + ''' + Stop recording and save the data to a file path + self.filename. Wait until the file is written. + Close the device afterwards. + + .. versionadded:: 1.4.0 + ''' + + # stop the recording first + send_command( + device=self.device, + msg=MCI_STOP, + flags=MCI_WAIT, + params=None + ) + + # choose filename for the WAV file + save_params = MCI_SAVE_PARMS() + save_params.lpfilename = self.filename + + # save the sound data to a file and wait + # until it ends writing to the file + send_command( + device=self.device, + msg=MCI_SAVE, + flags=MCI_SAVE_FILE | MCI_WAIT, + params=save_params + ) + + # close the recording device + send_command( + device=self.device, + msg=MCI_CLOSE, + flags=0, + params=None + ) + + +class WinPlayer: + ''' + Generic wrapper for MCI_PLAY handling the device closing. + + .. versionadded:: 1.4.0 + ''' + + def __init__(self, device): + self._device = device + + @property + def device(self): + ''' + Public property returning device ID. + + .. versionadded:: 1.4.0 + ''' + return self._device + + def play(self): + ''' + Start playing a WAV sound. + + .. versionadded:: 1.4.0 + ''' + play_params = MCI_PLAY_PARMS() + play_params.dwFrom = 0 + + send_command( + device=self.device, + msg=MCI_PLAY, + flags=MCI_FROM, + params=play_params + ) + + def stop(self): + ''' + Stop playing a WAV sound and close the device. + + .. versionadded:: 1.4.0 + ''' + send_command( + device=self.device, + msg=MCI_STOP, + flags=MCI_WAIT, + params=None + ) + + # close the playing device + send_command( + device=self.device, + msg=MCI_CLOSE, + flags=0, + params=None + ) + + +class WinAudio(Audio): + ''' + Windows implementation of audio recording and audio playing. + + .. versionadded:: 1.4.0 + ''' + + def __init__(self, file_path=None): + # default path unless specified otherwise + default_path = join( + WinStoragePath().get_music_dir(), + 'audio.wav' + ) + super().__init__(file_path or default_path) + + self._recorder = None + self._player = None + self._current_file = None + + def _start(self): + ''' + Start recording a WAV sound in the background asynchronously. + + .. versionadded:: 1.4.0 + ''' + + # clean everything before recording in case + # there is a different device open + self._stop() + + # create structure and set device parameters + open_params = MCI_OPEN_PARMS() + open_params.lpstrDeviceType = 'waveaudio' + open_params.lpstrElementName = '' + + # open a new device for recording + open_params = send_command( + device=0, # device ID before opening + msg=MCI_OPEN, + + # empty filename in lpstrElementName + # device type in lpstrDeviceType + flags=MCI_OPEN_ELEMENT | MCI_OPEN_TYPE, + params=open_params + ) + + # get recorder with device id and path for saving + self._recorder = WinRecorder( + device=open_params.wDeviceID, + filename=self._file_path + ) + self._recorder.record() + + # Setting the currently recorded file as current file + # for using it as a parameter in audio player + self._current_file = self._recorder.filename + + def _stop(self): + ''' + Stop recording or playing of a WAV sound. + + .. versionadded:: 1.4.0 + ''' + + if self._recorder: + self._recorder.stop() + self._recorder = None + + if self._player: + self._player.stop() + self._player = None + + def _play(self): + ''' + Play a WAV sound from a file. Prioritize latest recorded file before + default file path from WinAudio. + + .. versionadded:: 1.4.0 + ''' + + # create structure and set device parameters + open_params = MCI_OPEN_PARMS() + open_params.lpstrDeviceType = 'waveaudio' + open_params.lpstrElementName = self._current_file or self._file_path + + # open a new device for playing + open_params = send_command( + device=0, # device ID before opening + msg=MCI_OPEN, + + # existing filename in lpstrElementName + # device type in lpstrDeviceType + flags=MCI_OPEN_ELEMENT | MCI_OPEN_TYPE, + params=open_params + ) + + # get recorder with device id and path for saving + self._player = WinPlayer(device=open_params.wDeviceID) + self._player.play() + + +def instance(): + ''' + Instance for facade proxy. + ''' + return WinAudio() diff --git a/sbapp/plyer/platforms/win/battery.py b/sbapp/plyer/platforms/win/battery.py new file mode 100644 index 0000000..861d904 --- /dev/null +++ b/sbapp/plyer/platforms/win/battery.py @@ -0,0 +1,36 @@ +''' +Module of Windows API for plyer.battery. +''' + +from plyer.platforms.win.libs.batterystatus import battery_status +from plyer.facades import Battery +from ctypes.wintypes import BYTE + + +class WinBattery(Battery): + ''' + Implementation of Windows battery API. + ''' + + def _get_state(self): + CHARGING = BYTE(8).value + UNKNOWN_STATUS = BYTE(255).value + status = {"isCharging": None, "percentage": None} + + query = battery_status() + + if not query: + return status + + status["isCharging"] = (query["BatteryFlag"] != UNKNOWN_STATUS) and \ + (query["BatteryFlag"] & CHARGING > 0) + status["percentage"] = query["BatteryLifePercent"] + + return status + + +def instance(): + ''' + Instance for facade proxy. + ''' + return WinBattery() diff --git a/sbapp/plyer/platforms/win/cpu.py b/sbapp/plyer/platforms/win/cpu.py new file mode 100644 index 0000000..d1262dc --- /dev/null +++ b/sbapp/plyer/platforms/win/cpu.py @@ -0,0 +1,252 @@ +''' +Module of Windows API for plyer.cpu. +''' + +from ctypes import ( + c_ulonglong, c_ulong, byref, + Structure, POINTER, Union, windll, create_string_buffer, + sizeof, cast, c_void_p, c_uint32 +) +from ctypes.wintypes import ( + BYTE, DWORD, WORD +) + +from plyer.facades import CPU + + +KERNEL = windll.kernel32 +ERROR_INSUFFICIENT_BUFFER = 0x0000007A + + +class CacheType: + ''' + Win API PROCESSOR_CACHE_TYPE enum. + ''' + + unified = 0 + instruction = 1 + data = 2 + trace = 3 + + +class RelationshipType: + ''' + Win API LOGICAL_PROCESSOR_RELATIONSHIP enum. + ''' + + processor_core = 0 # logical proc sharing single core + numa_node = 1 # logical proc sharing single NUMA node + cache = 2 # logical proc sharing cache + processor_package = 3 # logical proc sharing physical package + group = 4 # logical proc sharing processor group + all = 0xffff # logical proc info for all groups + + +class CacheDescriptor(Structure): + ''' + Win API CACHE_DESCRIPTOR struct. + ''' + + _fields_ = [ + ('Level', BYTE), + ('Associativity', BYTE), + ('LineSize', WORD), + ('Size', DWORD), + ('Type', DWORD) + ] + + +class ProcessorCore(Structure): + ''' + Win API ProcessorCore struct. + ''' + + _fields_ = [('Flags', BYTE)] + + +class NumaNode(Structure): + ''' + Win API NumaNode struct. + ''' + + _fields_ = [('NodeNumber', DWORD)] + + +class SystemLPIUnion(Union): + ''' + Win API SYSTEM_LOGICAL_PROCESSOR_INFORMATION union without name. + ''' + + _fields_ = [ + ('ProcessorCore', ProcessorCore), + ('NumaNode', NumaNode), + ('Cache', CacheDescriptor), + ('Reserved', c_ulonglong) + ] + + +class SystemLPI(Structure): + ''' + Win API SYSTEM_LOGICAL_PROCESSOR_INFORMATION struct. + ''' + + _fields_ = [ + ('ProcessorMask', c_ulong), + ('Relationship', c_ulong), + ('LPI', SystemLPIUnion) + ] + + +class WinCPU(CPU): + ''' + Implementation of Windows CPU API. + ''' + + @staticmethod + def _countbits(mask): + # make sure the correct ULONG_PTR size is used on 64bit + # https://docs.microsoft.com/en-us/windows/ + # desktop/WinProg/windows-data-types + # note: not a pointer per-se, != PULONG_PTR + ulong_ptr = c_ulonglong if sizeof(c_void_p) == 8 else c_ulong + # note: c_ulonglong only on 64bit, otherwise c_ulong + + # DWORD == c_uint32 + # https://docs.microsoft.com/en-us/windows/ + # desktop/WinProg/windows-data-types + lshift = c_uint32(sizeof(ulong_ptr) * 8 - 1) + assert lshift.value in (31, 63), lshift # 32 or 64 bits - 1 + + lshift = lshift.value + test = 1 << lshift + assert test % 2 == 0, test + + count = 0 + i = 0 + while i <= lshift: + i += 1 + + # do NOT remove!!! + # test value has to be %2 == 0, + # except the last case where the value is 1, + # so that int(test) == int(float(test)) + # and the mask bit is counted correctly + assert test % 2 == 0 or float(test) == 1.0, test + + # https://stackoverflow.com/a/1746642/5994041 + # note: useful to print(str(bin(int(...)))[2:]) + count += 1 if (mask & int(test)) else 0 + test /= 2 + + return count + + def _logprocinfo(self, relationship): + get_logical_process_info = KERNEL.GetLogicalProcessorInformation + + # first call with no structure to get the real size of the required + buff_length = c_ulong(0) + result = get_logical_process_info(None, byref(buff_length)) + assert not result, result + error = KERNEL.GetLastError() + assert error == ERROR_INSUFFICIENT_BUFFER, error + assert buff_length, buff_length + + # create buffer from the real winapi buffer length + buff = create_string_buffer(buff_length.value) + + # call again with buffer pointer + the same length as arguments + result = get_logical_process_info(buff, byref(buff_length)) + assert result, (result, KERNEL.GetLastError()) + + # memory size of one LPI struct in the array of LPI structs + offset = sizeof(SystemLPI) # ok + values = { + key: 0 for key in ( + 'relationship', 'mask', + 'L1', 'L2', 'L3' + ) + } + + for i in range(0, buff_length.value, offset): + slpi = cast( + buff[i: i + offset], + POINTER(SystemLPI) + ).contents + + if slpi.Relationship != relationship: + continue + + values['relationship'] += 1 + values['mask'] += self._countbits(slpi.ProcessorMask) + + if slpi.LPI.Cache.Level == 1: + values['L1'] += 1 + elif slpi.LPI.Cache.Level == 2: + values['L2'] += 1 + elif slpi.LPI.Cache.Level == 3: + values['L3'] += 1 + + return values + + def _sockets(self): + # physical CPU sockets (or slots) on motherboard + return self._logprocinfo( + RelationshipType.processor_package + )['relationship'] + + def _physical(self): + # cores + return self._logprocinfo( + RelationshipType.processor_core + )['relationship'] + + def _logical(self): + # cores * threads + # if hyperthreaded core -> more than one logical processor + return self._logprocinfo( + RelationshipType.processor_core + )['mask'] + + def _cache(self): + # L1, L2, L3 cache count + result = self._logprocinfo( + RelationshipType.cache + ) + return { + key: result[key] + for key in result + if key in ('L1', 'L2', 'L3') + } + + def _numa(self): + # numa nodes + return self._logprocinfo( + RelationshipType.numa_node + )['relationship'] + + +def instance(): + ''' + Instance for facade proxy. + ''' + return WinCPU() + + +# Resources: +# GetLogicalProcessInformation +# https://msdn.microsoft.com/en-us/library/ms683194(v=vs.85).aspx + +# SYSTEM_LOGICAL_PROCESSOR_INFORMATION +# https://msdn.microsoft.com/en-us/library/ms686694(v=vs.85).aspx + +# LOGICAL_PROCESSOR_RELATIONSHIP enum (0 - 4, 0xffff) +# https://msdn.microsoft.com/2ada52f0-70ec-4146-9ef7-9af3b08996f9 + +# CACHE_DESCRIPTOR struct +# https://msdn.microsoft.com/38cfa605-831c-45ef-a99f-55f42b2b56e9 + +# PROCESSOR_CACHE_TYPE +# https://msdn.microsoft.com/23044f67-e944-43c2-8c75-3d2fba87cb3c + +# C example +# https://msdn.microsoft.com/en-us/904d2d35-f419-4e8f-a689-f39ed926644c diff --git a/sbapp/plyer/platforms/win/devicename.py b/sbapp/plyer/platforms/win/devicename.py new file mode 100644 index 0000000..d35d76b --- /dev/null +++ b/sbapp/plyer/platforms/win/devicename.py @@ -0,0 +1,23 @@ +''' +Module of Win API for plyer.devicename. +''' + +import socket +from plyer.facades import DeviceName + + +class WinDeviceName(DeviceName): + ''' + Implementation of Linux DeviceName API. + ''' + + def _get_device_name(self): + hostname = socket.gethostname() + return hostname + + +def instance(): + ''' + Instance for facade proxy. + ''' + return WinDeviceName() diff --git a/sbapp/plyer/platforms/win/email.py b/sbapp/plyer/platforms/win/email.py new file mode 100644 index 0000000..4c0f9b7 --- /dev/null +++ b/sbapp/plyer/platforms/win/email.py @@ -0,0 +1,46 @@ +''' +Module of Windows API for plyer.email. +''' + +import os +try: + from urllib.parse import quote +except ImportError: + from urllib import quote +from plyer.facades import Email + + +class WindowsEmail(Email): + ''' + Implementation of Windows email API. + ''' + + def _send(self, **kwargs): + recipient = kwargs.get('recipient') + subject = kwargs.get('subject') + text = kwargs.get('text') + + uri = "mailto:" + if recipient: + uri += str(recipient) + if subject: + uri += "?" if "?" not in uri else "&" + uri += "subject=" + uri += quote(str(subject)) + if text: + uri += "?" if "?" not in uri else "&" + uri += "body=" + uri += quote(str(text)) + + # WE + startfile are available only on Windows + try: + os.startfile(uri) + except WindowsError: + print("Warning: unable to find a program able to send emails.") + + +def instance(): + ''' + Instance for facade proxy. + ''' + return WindowsEmail() diff --git a/sbapp/plyer/platforms/win/filechooser.py b/sbapp/plyer/platforms/win/filechooser.py new file mode 100644 index 0000000..d61fdc1 --- /dev/null +++ b/sbapp/plyer/platforms/win/filechooser.py @@ -0,0 +1,162 @@ +''' +Windows file chooser +-------------------- +''' + +from plyer.facades import FileChooser +from win32com.shell.shell import ( + SHBrowseForFolder as browse, + SHGetPathFromIDList as get_path +) +from win32com.shell import shellcon +import win32gui +import win32con +import pywintypes +from os.path import dirname, splitext, join, isdir + + +class Win32FileChooser: + '''A native implementation of NativeFileChooser using the + Win32 API on Windows. + + Not Implemented features (all dialogs): + * preview + * icon + + Not implemented features (in directory selection only - it's limited + by Windows itself): + * preview + * window-icon + + Known issues: + * non-existins folders such as: Network, Control Panel, My Computer, Trash, + Library and likes will raise a COM error. The path does not exist, nor + a user can open from or save to such path. + ''' + + path = None + multiple = False + filters = [] + preview = False + title = None + icon = None + show_hidden = False + + def __init__(self, *args, **kwargs): + self._handle_selection = kwargs.pop( + 'on_selection', self._handle_selection + ) + + # Simulate Kivy's behavior + for i in kwargs: + setattr(self, i, kwargs[i]) + + @staticmethod + def _handle_selection(selection): + ''' + Dummy placeholder for returning selection from chooser. + ''' + return selection + + def run(self): + self.selection = [] + try: + if self.mode != "dir": + args = {} + + if self.path: + if isdir(self.path): + args["InitialDir"] = self.path + else: + args["InitialDir"] = dirname(self.path) + _, ext = splitext(self.path) + args["File"] = self.path + args["DefExt"] = ext and ext[1:] # no period + + args["Title"] = self.title if self.title else "Pick a file..." + args["CustomFilter"] = 'Other file types\x00*.*\x00' + args["FilterIndex"] = 1 + file = "" + if "File" in args: + file = args["File"] + args["File"] = file + ("\x00" * 4096) + + # e.g. open_file(filters=['*.txt', '*.py']) + filters = "" + for f in self.filters: + if type(f) == str: + filters += (f + "\x00") * 2 + else: + filters += f[0] + "\x00" + ";".join(f[1:]) + "\x00" + args["Filter"] = filters + + flags = win32con.OFN_OVERWRITEPROMPT + flags |= win32con.OFN_HIDEREADONLY + + if self.multiple: + flags |= win32con.OFN_ALLOWMULTISELECT + flags |= win32con.OFN_EXPLORER + if self.show_hidden: + flags |= win32con.OFN_FORCESHOWHIDDEN + + args["Flags"] = flags + + try: + if self.mode == "open": + self.fname, _, _ = win32gui.GetOpenFileNameW(**args) + elif self.mode == "save": + self.fname, _, _ = win32gui.GetSaveFileNameW(**args) + except pywintypes.error as e: + # if canceled, it's not really an error + if not e.winerror: + self._handle_selection(self.selection) + return self.selection + raise + + if self.fname: + if self.multiple: + seq = str(self.fname).split("\x00") + if len(seq) > 1: + dir_n, base_n = seq[0], seq[1:] + self.selection = [ + join(dir_n, i) for i in base_n + ] + else: + self.selection = seq + else: + self.selection = str(self.fname).split("\x00") + + else: # dir mode + BIF_EDITBOX = shellcon.BIF_EDITBOX + BIF_NEWDIALOGSTYLE = 0x00000040 + # From http://goo.gl/UDqCqo + pidl, name, images = browse( + win32gui.GetDesktopWindow(), + None, + self.title if self.title else "Pick a folder...", + BIF_NEWDIALOGSTYLE | BIF_EDITBOX, None, None + ) + + # pidl is None when nothing is selected + # and e.g. the dialog is closed afterwards with Cancel + if pidl: + self.selection = [str(get_path(pidl).decode('utf-8'))] + + except (RuntimeError, pywintypes.error, Exception): + # ALWAYS! let user know what happened + import traceback + traceback.print_exc() + self._handle_selection(self.selection) + return self.selection + + +class WinFileChooser(FileChooser): + '''FileChooser implementation for Windows, using win3all. + ''' + + def _file_selection_dialog(self, **kwargs): + return Win32FileChooser(**kwargs).run() + + +def instance(): + return WinFileChooser() diff --git a/sbapp/plyer/platforms/win/keystore.py b/sbapp/plyer/platforms/win/keystore.py new file mode 100644 index 0000000..0065a6a --- /dev/null +++ b/sbapp/plyer/platforms/win/keystore.py @@ -0,0 +1,19 @@ +try: + import keyring +except Exception: + raise NotImplementedError() + +from plyer.facades import Keystore + + +class WinKeystore(Keystore): + + def _set_key(self, servicename, key, value, **kwargs): + keyring.set_password(servicename, key, value) + + def _get_key(self, servicename, key, **kwargs): + return keyring.get_password(servicename, key) + + +def instance(): + return WinKeystore() diff --git a/sbapp/plyer/platforms/win/libs/__init__.py b/sbapp/plyer/platforms/win/libs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sbapp/plyer/platforms/win/libs/balloontip.py b/sbapp/plyer/platforms/win/libs/balloontip.py new file mode 100644 index 0000000..5494f4e --- /dev/null +++ b/sbapp/plyer/platforms/win/libs/balloontip.py @@ -0,0 +1,206 @@ +# -- coding: utf-8 -- +''' +Module of Windows API for creating taskbar balloon tip +notification in the taskbar's tray notification area. +''' + +__all__ = ('WindowsBalloonTip', 'balloon_tip') + + +import time +import ctypes +import atexit +from threading import RLock + +from plyer.platforms.win.libs import win_api_defs + + +WS_OVERLAPPED = 0x00000000 +WS_SYSMENU = 0x00080000 +WM_DESTROY = 2 +CW_USEDEFAULT = 8 + +LR_LOADFROMFILE = 16 +LR_DEFAULTSIZE = 0x0040 +IDI_APPLICATION = 32512 +IMAGE_ICON = 1 + +NOTIFYICON_VERSION_4 = 4 +NIM_ADD = 0 +NIM_MODIFY = 1 +NIM_DELETE = 2 +NIM_SETVERSION = 4 +NIF_MESSAGE = 1 +NIF_ICON = 2 +NIF_TIP = 4 +NIF_INFO = 0x10 +NIIF_USER = 4 +NIIF_LARGE_ICON = 0x20 + + +class WindowsBalloonTip: + ''' + Implementation of balloon tip notifications through Windows API. + + * Register Window class name: + https://msdn.microsoft.com/en-us/library/windows/desktop/ms632596.aspx + * Create an overlapped window using the registered class. + - It's hidden everywhere in GUI unless ShowWindow(handle, SW_SHOW) + function is called. + * Show/remove a tray icon and a balloon tip notification. + + Each instance is a separate notification with different parameters. + Can be used with Threads. + ''' + + _class_atom = 0 + _wnd_class_ex = None + _hwnd = None + _hicon = None + _balloon_icon = None + _notify_data = None + _count = 0 + _lock = RLock() + + @staticmethod + def _get_unique_id(): + ''' + Keep track of each created balloon tip notification names, + so that they can be easily identified even from outside. + + Make sure the count is shared between all the instances + i.e. use a lock, so that _count class variable is incremented + safely when using balloon tip notifications with Threads. + ''' + + WindowsBalloonTip._lock.acquire() + val = WindowsBalloonTip._count + WindowsBalloonTip._count += 1 + WindowsBalloonTip._lock.release() + return val + + def __init__(self, title, message, app_name, app_icon='', + timeout=10, **kwargs): + ''' + The app_icon parameter, if given, is an .ICO file. + ''' + atexit.register(self.__del__) + + wnd_class_ex = win_api_defs.get_WNDCLASSEXW() + class_name = 'PlyerTaskbar' + str(WindowsBalloonTip._get_unique_id()) + + wnd_class_ex.lpszClassName = class_name + + # keep ref to it as long as window is alive + wnd_class_ex.lpfnWndProc = win_api_defs.WindowProc( + win_api_defs.DefWindowProcW + ) + wnd_class_ex.hInstance = win_api_defs.GetModuleHandleW(None) + if wnd_class_ex.hInstance is None: + raise Exception('Could not get windows module instance.') + + class_atom = win_api_defs.RegisterClassExW(wnd_class_ex) + if class_atom == 0: + raise Exception('Could not register the PlyerTaskbar class.') + + self._class_atom = class_atom + self._wnd_class_ex = wnd_class_ex + + # create window + self._hwnd = win_api_defs.CreateWindowExW( + # dwExStyle, lpClassName, lpWindowName, dwStyle + 0, class_atom, '', WS_OVERLAPPED, + # x, y, nWidth, nHeight + 0, 0, CW_USEDEFAULT, CW_USEDEFAULT, + # hWndParent, hMenu, hInstance, lpParam + None, None, wnd_class_ex.hInstance, None + ) + if self._hwnd is None: + raise Exception('Could not get create window.') + win_api_defs.UpdateWindow(self._hwnd) + + # load .ICO file for as balloon tip and tray icon + if app_icon: + icon_flags = LR_LOADFROMFILE | LR_DEFAULTSIZE + hicon = win_api_defs.LoadImageW( + None, app_icon, IMAGE_ICON, 0, 0, icon_flags + ) + + if hicon is None: + raise Exception('Could not load icon {}'.format(app_icon)) + self._balloon_icon = self._hicon = hicon + else: + self._hicon = win_api_defs.LoadIconW( + None, + ctypes.cast(IDI_APPLICATION, win_api_defs.LPCWSTR) + ) + + # show the notification + self.notify(title, message, app_name) + if timeout: + time.sleep(timeout) + + def __del__(self): + ''' + Clean visible parts of the notification object, then free all resources + allocated for creating the nofitication Window and icon. + ''' + self.remove_notify() + if self._hicon is not None: + win_api_defs.DestroyIcon(self._hicon) + if self._wnd_class_ex is not None: + win_api_defs.UnregisterClassW( + self._class_atom, + self._wnd_class_ex.hInstance + ) + if self._hwnd is not None: + win_api_defs.DestroyWindow(self._hwnd) + + def notify(self, title, message, app_name): + ''' + Displays a balloon in the systray. Can be called multiple times + with different parameter values. + ''' + # remove previous visible balloon tip nofitication if available + self.remove_notify() + + # add icon and messages to window + hicon = self._hicon + flags = NIF_TIP | NIF_INFO + icon_flag = 0 + + if hicon is not None: + flags |= NIF_ICON + + # if icon is default app's one, don't display it in message + if self._balloon_icon is not None: + icon_flag = NIIF_USER | NIIF_LARGE_ICON + + notify_data = win_api_defs.get_NOTIFYICONDATAW( + 0, self._hwnd, + id(self), flags, 0, hicon, app_name, 0, 0, message, + NOTIFYICON_VERSION_4, title, icon_flag, win_api_defs.GUID(), + self._balloon_icon + ) + + self._notify_data = notify_data + if not win_api_defs.Shell_NotifyIconW(NIM_ADD, notify_data): + raise Exception('Shell_NotifyIconW failed.') + if not win_api_defs.Shell_NotifyIconW(NIM_SETVERSION, + notify_data): + raise Exception('Shell_NotifyIconW failed.') + + def remove_notify(self): + ''' + Removes the notify balloon, if displayed. + ''' + if self._notify_data is not None: + win_api_defs.Shell_NotifyIconW(NIM_DELETE, self._notify_data) + self._notify_data = None + + +def balloon_tip(**kwargs): + ''' + Instance for balloon tip notification implementation. + ''' + WindowsBalloonTip(**kwargs) diff --git a/sbapp/plyer/platforms/win/libs/batterystatus.py b/sbapp/plyer/platforms/win/libs/batterystatus.py new file mode 100644 index 0000000..1d8202d --- /dev/null +++ b/sbapp/plyer/platforms/win/libs/batterystatus.py @@ -0,0 +1,24 @@ +''' +Module of Windows API helper for plyer.battery. +''' + +__all__ = ('battery_status') + + +import ctypes +from plyer.platforms.win.libs import win_api_defs + + +def battery_status(): + ''' + Implementation of Windows system power status API for plyer.battery. + ''' + + status = win_api_defs.SYSTEM_POWER_STATUS() + if not win_api_defs.GetSystemPowerStatus(ctypes.pointer(status)): + raise Exception('Could not get system power status.') + + return dict( + (field, getattr(status, field)) + for field, _ in status._fields_ + ) diff --git a/sbapp/plyer/platforms/win/libs/wifi_defs.py b/sbapp/plyer/platforms/win/libs/wifi_defs.py new file mode 100644 index 0000000..2899828 --- /dev/null +++ b/sbapp/plyer/platforms/win/libs/wifi_defs.py @@ -0,0 +1,572 @@ +''' +Reference +Methods, Structures and Documentation adapted from. +https://msdn.microsoft.com/en-us/library/windows/desktop \ +/ms705945%28v=vs.85%29.aspx +''' + +from ctypes import ( + POINTER, + FormatError, + Structure, + addressof, + byref, + c_bool, + c_char, + c_ubyte, + c_uint, + c_ulong, + c_ushort, + c_void_p, + c_wchar, + pointer, + windll, +) +from ctypes.wintypes import DWORD, HANDLE, LPCWSTR, ULONG +from sys import exit as sys_exit + + +def customresize(array, new_size): + return ( + array._type_ * new_size + ).from_address( + addressof(array) + ) + + +wlanapi = windll.LoadLibrary('wlanapi.dll') + + +class GUID(Structure): + _fields_ = [ + ('Data1', c_ulong), + ('Data2', c_ushort), + ('Data3', c_ushort), + ('Data4', c_ubyte * 8), + ] + + +# The WLAN_INTERFACE_STATE enumerated type indicates the state of an interface. +WLAN_INTERFACE_STATE = c_uint +(wlan_interface_state_not_ready, + wlan_interface_state_connected, + wlan_interface_state_ad_hoc_network_formed, + wlan_interface_state_disconnecting, + wlan_interface_state_disconnected, + wlan_interface_state_associating, + wlan_interface_state_discovering, + wlan_interface_state_authenticating) = map(WLAN_INTERFACE_STATE, + range(0, 8)) + + +class WLAN_INTERFACE_INFO(Structure): + ''' + The WLAN_INTERFACE_STATE enumerated type indicates the state of an + interface. + ''' + _fields_ = [ + ("InterfaceGuid", GUID), + ("strInterfaceDescription", c_wchar * 256), + ("isState", WLAN_INTERFACE_STATE) + ] + + +class WLAN_INTERFACE_INFO_LIST(Structure): + ''' + The WLAN_INTERFACE_INFO_LIST structure contains an array of NIC interface + information. + ''' + _fields_ = [ + ("NumberOfItems", DWORD), + ("Index", DWORD), + ("InterfaceInfo", WLAN_INTERFACE_INFO * 1) + ] + + +DOT11_MAC_ADDRESS = c_ubyte * 6 +WLAN_MAX_PHY_TYPE_NUMBER = 0x8 +DOT11_SSID_MAX_LENGTH = 32 +WLAN_REASON_CODE = DWORD + +DOT11_BSS_TYPE = c_uint +(dot11_BSS_type_infrastructure, + dot11_BSS_type_independent, + dot11_BSS_type_any) = map(DOT11_BSS_TYPE, range(1, 4)) + +# The DOT11_PHY_TYPE enumeration defines an 802.11 PHY and media type. +DOT11_PHY_TYPE = c_uint +dot11_phy_type_unknown = 0 +dot11_phy_type_any = 0 +dot11_phy_type_fhss = 1 +dot11_phy_type_dsss = 2 +dot11_phy_type_irbaseband = 3 +dot11_phy_type_ofdm = 4 +dot11_phy_type_hrdsss = 5 +dot11_phy_type_erp = 6 +dot11_phy_type_ht = 7 +dot11_phy_type_IHV_start = 0x80000000 +dot11_phy_type_IHV_end = 0xffffffff + +# The DOT11_AUTH_ALGORITHM enumerated type defines a wireless +# LAN authentication algorithm. +DOT11_AUTH_ALGORITHM = c_uint +DOT11_AUTH_ALGO_80211_OPEN = 1 +DOT11_AUTH_ALGO_80211_SHARED_KEY = 2 +DOT11_AUTH_ALGO_WPA = 3 +DOT11_AUTH_ALGO_WPA_PSK = 4 +DOT11_AUTH_ALGO_WPA_NONE = 5 +DOT11_AUTH_ALGO_RSNA = 6 +DOT11_AUTH_ALGO_RSNA_PSK = 7 +DOT11_AUTH_ALGO_IHV_START = 0x80000000 +DOT11_AUTH_ALGO_IHV_END = 0xffffffff + +# The DOT11_CIPHER_ALGORITHM enumerated type defines a cipher +# algorithm for data encryption and decryption. +DOT11_CIPHER_ALGORITHM = c_uint +DOT11_CIPHER_ALGO_NONE = 0x00 +DOT11_CIPHER_ALGO_WEP40 = 0x01 +DOT11_CIPHER_ALGO_TKIP = 0x02 +DOT11_CIPHER_ALGO_CCMP = 0x04 +DOT11_CIPHER_ALGO_WEP104 = 0x05 +DOT11_CIPHER_ALGO_WPA_USE_GROUP = 0x100 +DOT11_CIPHER_ALGO_RSN_USE_GROUP = 0x100 +DOT11_CIPHER_ALGO_WEP = 0x101 +DOT11_CIPHER_ALGO_IHV_START = 0x80000000 +DOT11_CIPHER_ALGO_IHV_END = 0xffffffff + + +class DOT11_SSID(Structure): + ''' + A DOT11_SSID structure contains the SSID of an interface + ''' + _fields_ = [ + ("SSIDLength", c_ulong), + ("SSID", c_char * DOT11_SSID_MAX_LENGTH) + ] + + +# Enumerated type to define the code of connection. +WLAN_CONNECTION_MODE = c_uint +(wlan_connection_mode_profile, + wlan_connection_mode_temporary_profile, + wlan_connection_mode_discovery_secure, + wlan_connection_mode_discovery_unsecure, + wlan_connection_mode_auto, + wlan_connection_mode_invalid) = map(WLAN_CONNECTION_MODE, range(0, 6)) + + +class NDIS_OBJECT_HEADER(Structure): + ''' + This Structure packages the object type, version, and size information + that is required in many NDIS (Netword Driver interface Specification) + Structures. + ''' + _fields_ = [ + ("Type", c_char), + ("Revision", c_char), + ("Size", c_ushort)] + + +class DOT11_BSSID_LIST(Structure): + ''' + The DOT11_BSSID_LIST structure contains a list of basic service set (BSS) + identifiers. + ''' + _fields_ = [ + ("Header", NDIS_OBJECT_HEADER), + ("uNumOfEntries", ULONG), + ("uTotalNumOfEntries", ULONG), + ("BSSIDs", DOT11_MAC_ADDRESS * 1) + ] + + +class WLAN_CONNECTION_PARAMETERS(Structure): + ''' + The WLAN_CONNECTION_PARAMETERS structure specifies the parameters used when + using the WlanConnect function. + ''' + _fields_ = [ + ("wlanConnectionMode", WLAN_CONNECTION_MODE), + ("strProfile", LPCWSTR), + ("pDot11Ssid", POINTER(DOT11_SSID)), + ("pDesiredBssidList", POINTER(DOT11_BSSID_LIST)), + ("dot11BssType", DOT11_BSS_TYPE), + ("dwFlags", DWORD)] + + +# The `WlanConnect` attempts to connect to a specific network. +WlanConnect = wlanapi.WlanConnect +WlanConnect.argtypes = (HANDLE, + POINTER(GUID), + POINTER(WLAN_CONNECTION_PARAMETERS), + c_void_p) +WlanConnect.restype = DWORD + +# The `WlanDisconnect` method disconnects an interface from its +# current network. +WlanDisconnect = wlanapi.WlanDisconnect +WlanDisconnect.argtypes = (HANDLE, + POINTER(GUID), + c_void_p) +WlanDisconnect.restype = DWORD + +# Opens a connection to the server. +WlanOpenHandle = wlanapi.WlanOpenHandle +WlanOpenHandle.argtypes = (DWORD, c_void_p, POINTER(DWORD), POINTER(HANDLE)) +WlanOpenHandle.restype = DWORD + +# The WlanCloseHandle method closes the connection to the server. +WlanCloseHandle = wlanapi.WlanCloseHandle +WlanCloseHandle.argtypes = (HANDLE, c_void_p) +WlanCloseHandle.restype = DWORD + + +class WLAN_AVAILABLE_NETWORK(Structure): + ''' + The WLAN_INTERFACE_INFO structure contains information about a wireless + LAN interface. + ''' + _fields_ = [ + ("ProfileName", c_wchar * 256), + ("dot11Ssid", DOT11_SSID), + ("dot11BssType", DOT11_BSS_TYPE), + ("NumberOfBssids", c_ulong), + ("NetworkConnectable", c_bool), + ("wlanNotConnectableReason", WLAN_REASON_CODE), + ("NumberOfPhyTypes", c_ulong), + ("dot11PhyTypes", DOT11_PHY_TYPE * WLAN_MAX_PHY_TYPE_NUMBER), + ("MorePhyTypes", c_bool), + ("wlanSignalQuality", c_ulong), + ("SecurityEnabled", c_bool), + ("dot11DefaultAuthAlgorithm", DOT11_AUTH_ALGORITHM), + ("dot11DefaultCipherAlgorithm", DOT11_CIPHER_ALGORITHM), + ("Flags", DWORD), + ("Reserved", DWORD)] + + +class WLAN_AVAILABLE_NETWORK_LIST(Structure): + ''' + The WLAN_INTERFACE_INFO_LIST structure contains an array of NIC + interface information. + ''' + _fields_ = [ + ("NumberOfItems", DWORD), + ("Index", DWORD), + ("Network", WLAN_AVAILABLE_NETWORK * 1)] + + +# The WlanEnumInterfaces function enumerates all of the wireless LAN interfaces +# currently enabled on the local computer. +WlanEnumInterfaces = wlanapi.WlanEnumInterfaces +WlanEnumInterfaces.argtypes = (HANDLE, + c_void_p, + POINTER(POINTER(WLAN_INTERFACE_INFO_LIST))) +WlanEnumInterfaces.restype = DWORD + +# The WlanGetAvailableNetworkList function retrieves the list of available +# networks on a wireless LAN interface. +WlanGetAvailableNetworkList = wlanapi.WlanGetAvailableNetworkList +WlanGetAvailableNetworkList.argtypes = (HANDLE, + POINTER(GUID), + DWORD, + c_void_p, + POINTER(POINTER( + WLAN_AVAILABLE_NETWORK_LIST))) +WlanGetAvailableNetworkList.restype = DWORD + +# The WlanFreeMemory function frees memory. Any memory returned from Native +# Wifi functions must be freed. +WlanFreeMemory = wlanapi.WlanFreeMemory +WlanFreeMemory.argtypes = [c_void_p] + +wireless_interfaces = None +available = None +_dict = {} + +# Private methods. + + +def _connect(network, parameters): + ''' + Attempts to connect to a specific network. + ''' + global _dict + wireless_interface = _dict[network] + + wcp = WLAN_CONNECTION_PARAMETERS() + connection_mode = parameters['connection_mode'] + wcp.wlanConnectionMode = WLAN_CONNECTION_MODE(connection_mode) + + if connection_mode == 0 or connection_mode == 1: + wcp.strProfile = LPCWSTR(parameters["profile"]) + else: + wcp.strProfile = None + + dot11Ssid = DOT11_SSID() + try: + dot11Ssid.SSID = parameters["ssid"] + dot11Ssid.SSIDLength = len(parameters["ssid"]) + except KeyError: + dot11Ssid.SSID = network + dot11Ssid.SSIDLength = len(network) + wcp.pDot11Ssid = pointer(dot11Ssid) + + dot11bssid = DOT11_BSSID_LIST() + bssid = parameters["bssidList"] + dot11bssid.Header = bssid['Header'] + dot11bssid.uNumOfEntries = bssid['uNumOfEntries'] + dot11bssid.uTotalNumOfEntries = bssid['uTotalNumOfEntries'] + dot11bssid.BSSIDs = bssid['BSSIDs'] + + wcp.pDesiredBssidList = pointer(dot11bssid) + + bssType = parameters["bssType"] + wcp.dot11BssType = DOT11_BSS_TYPE(bssType) + + wcp.dwFlags = DWORD(parameters["flags"]) + + NegotiatedVersion = DWORD() + ClientHandle = HANDLE() + + wlan = WlanOpenHandle(1, + None, + byref(NegotiatedVersion), + byref(ClientHandle)) + if wlan: + sys_exit(FormatError(wlan)) + pInterfaceList = pointer(WLAN_INTERFACE_INFO_LIST()) + wlan = WlanEnumInterfaces(ClientHandle, None, byref(pInterfaceList)) + if wlan: + sys_exit(FormatError(wlan)) + + try: + wlan = WlanConnect(ClientHandle, + wireless_interface, + wcp, + None) + if wlan: + sys_exit(FormatError(wlan)) + WlanCloseHandle(ClientHandle, None) + finally: + WlanFreeMemory(pInterfaceList) + + +def _disconnect(): + ''' + To disconnect an interface form the current network. + ''' + NegotiatedVersion = DWORD() + ClientHandle = HANDLE() + + wlan = WlanOpenHandle( + 1, + None, + byref(NegotiatedVersion), + byref(ClientHandle) + ) + if wlan: + sys_exit(FormatError(wlan)) + + pInterfaceList = pointer(WLAN_INTERFACE_INFO_LIST()) + + wlan = WlanEnumInterfaces(ClientHandle, None, byref(pInterfaceList)) + if wlan: + sys_exit(FormatError(wlan)) + + result = None + try: + ifaces = customresize( + pInterfaceList.contents.InterfaceInfo, + pInterfaceList.contents.NumberOfItems + ) + + # find each available network for each interface + for iface in ifaces: + wlan = WlanDisconnect( + ClientHandle, + byref(iface.InterfaceGuid), + None + ) + if wlan: + sys_exit(FormatError(wlan)) + WlanCloseHandle(ClientHandle, None) + + finally: + WlanFreeMemory(pInterfaceList) + result = get_available_wifi() + + return result + + +def _start_scanning(): + ''' + Private method for scanning and returns the available devices. + ''' + global available + global wireless_interfaces + NegotiatedVersion = DWORD() + ClientHandle = HANDLE() + + wlan = WlanOpenHandle( + 1, + None, + byref(NegotiatedVersion), + byref(ClientHandle) + ) + + if wlan: + sys_exit(FormatError(wlan)) + + # find all wireless network interfaces + pInterfaceList = pointer(WLAN_INTERFACE_INFO_LIST()) + wlan = WlanEnumInterfaces(ClientHandle, None, byref(pInterfaceList)) + if wlan: + sys_exit(FormatError(wlan)) + + result = None + try: + ifaces = customresize( + pInterfaceList.contents.InterfaceInfo, + pInterfaceList.contents.NumberOfItems + ) + + # find each available network for each interface + wireless_interfaces = ifaces + for iface in ifaces: + pAvailableNetworkList = pointer(WLAN_AVAILABLE_NETWORK_LIST()) + wlan = WlanGetAvailableNetworkList( + ClientHandle, + byref(iface.InterfaceGuid), + 0, + None, + byref(pAvailableNetworkList) + ) + + if wlan: + sys_exit(FormatError(wlan)) + + try: + avail_net_list = pAvailableNetworkList.contents + networks = customresize( + avail_net_list.Network, + avail_net_list.NumberOfItems + ) + + # Assigning the value of networks to the global variable + # `available`, so it could be used in other methods. + available = networks + _make_dict() + wlan = WlanDisconnect( + ClientHandle, + byref(iface.InterfaceGuid), + None + ) + + if wlan: + sys_exit(FormatError(wlan)) + WlanCloseHandle(ClientHandle, None) + + finally: + WlanFreeMemory(pAvailableNetworkList) + + finally: + WlanFreeMemory(pInterfaceList) + result = get_available_wifi() + + return result + + +def _get_network_info(name): + ''' + returns the list of the network selected in a dict. + ''' + global available + global _dict + + net = _dict[name] + dot11BssType = net.dot11BssType + dot11DefaultAuthAlgorithm = net.dot11DefaultAuthAlgorithm + dot11DefaultCipherAlgorithm = net.dot11DefaultCipherAlgorithm + dot11PhyTypes = net.dot11PhyTypes[0] + dot11Ssid = net.dot11Ssid + wlanNotConnectableReason = net.wlanNotConnectableReason + wlanSignalQuality = net.wlanSignalQuality + return {"dot11BssType": dot11BssType, + "dot11DefaultAuthAlgorithm": dot11DefaultAuthAlgorithm, + "dot11DefaultCipherAlgorithm": dot11DefaultCipherAlgorithm, + "dot11PhyTypes": dot11PhyTypes, + "SSID": dot11Ssid.SSID, + "SSIDLength": dot11Ssid.SSIDLength, + "wlanNotConnectableReason": wlanNotConnectableReason, + "wlanSignalQuality": wlanSignalQuality} + + +def _make_dict(): + ''' + Prepares a dict so it could store network information. + ''' + global available + global _dict + _dict = {} + for network in available: + _dict[network.dot11Ssid.SSID.decode('utf-8')] = network + + +def _get_available_wifi(): + ''' + returns the available wifi networks. + ''' + global _dict + return _dict + + +def _is_enabled(): + ''' + Reason for returning true is explained in widi facade. + /plyer/facades/wifi.py + ''' + return True + +# public methods. + + +def is_enabled(): + ''' + calls private method `_is_enabled` and returns the result. + ''' + return _is_enabled() + + +def connect(network, parameters): + ''' + Connect to a network with following parameters. + ''' + _connect(network=network, parameters=parameters) + + +def disconnect(): + ''' + Disconnect from a network. + ''' + _disconnect() + + +def start_scanning(): + ''' + Start scanning for available wifi networks available. + ''' + return _start_scanning() + + +def get_network_info(name): + ''' + return the wifi network info. + ''' + return _get_network_info(name=name) + + +def get_available_wifi(): + ''' + return the available wifi networks available + ''' + return _get_available_wifi() diff --git a/sbapp/plyer/platforms/win/libs/win_api_defs.py b/sbapp/plyer/platforms/win/libs/win_api_defs.py new file mode 100755 index 0000000..dcf116b --- /dev/null +++ b/sbapp/plyer/platforms/win/libs/win_api_defs.py @@ -0,0 +1,250 @@ +''' Defines ctypes windows api. +''' + +__all__ = ('GUID', 'get_DLLVERSIONINFO', 'MAKEDLLVERULL', + 'get_NOTIFYICONDATAW', 'CreateWindowExW', 'WindowProc', + 'DefWindowProcW', 'get_WNDCLASSEXW', 'GetModuleHandleW', + 'RegisterClassExW', 'UpdateWindow', 'LoadImageW', + 'Shell_NotifyIconW', 'DestroyIcon', 'UnregisterClassW', + 'DestroyWindow', 'LoadIconW', 'get_PATH') + +import ctypes +from ctypes import Structure, windll, sizeof, POINTER, WINFUNCTYPE +from ctypes.wintypes import ( + DWORD, HICON, HWND, UINT, WCHAR, WORD, BYTE, + LPCWSTR, INT, LPVOID, HINSTANCE, HMENU, LPARAM, + WPARAM, HBRUSH, HMODULE, ATOM, BOOL, HANDLE +) +LRESULT = LPARAM +HRESULT = HANDLE +HCURSOR = HICON + + +class GUID(Structure): + _fields_ = [ + ('Data1', DWORD), + ('Data2', WORD), + ('Data3', WORD), + ('Data4', BYTE * 8) + ] + + +class DLLVERSIONINFO(Structure): + _fields_ = [ + ('cbSize', DWORD), + ('dwMajorVersion', DWORD), + ('dwMinorVersion', DWORD), + ('dwBuildNumber', DWORD), + ('dwPlatformID', DWORD), + ] + + +def get_DLLVERSIONINFO(*largs): + version_info = DLLVERSIONINFO(*largs) + version_info.cbSize = sizeof(DLLVERSIONINFO) + return version_info + + +def MAKEDLLVERULL(major, minor, build, sp): + return (major << 48) | (minor << 32) | (build << 16) | sp + + +NOTIFYICONDATAW_fields = [ + ("cbSize", DWORD), + ("hWnd", HWND), + ("uID", UINT), + ("uFlags", UINT), + ("uCallbackMessage", UINT), + ("hIcon", HICON), + ("szTip", WCHAR * 128), + ("dwState", DWORD), + ("dwStateMask", DWORD), + ("szInfo", WCHAR * 256), + ("uVersion", UINT), + ("szInfoTitle", WCHAR * 64), + ("dwInfoFlags", DWORD), + ("guidItem", GUID), + ("hBalloonIcon", HICON), +] + + +class NOTIFYICONDATAW(Structure): + _fields_ = NOTIFYICONDATAW_fields[:] + + +class NOTIFYICONDATAW_V3(Structure): + _fields_ = NOTIFYICONDATAW_fields[:-1] + + +class NOTIFYICONDATAW_V2(Structure): + _fields_ = NOTIFYICONDATAW_fields[:-2] + + +class NOTIFYICONDATAW_V1(Structure): + _fields_ = NOTIFYICONDATAW_fields[:6] + + +NOTIFYICONDATA_V3_SIZE = sizeof(NOTIFYICONDATAW_V3) +NOTIFYICONDATA_V2_SIZE = sizeof(NOTIFYICONDATAW_V2) +NOTIFYICONDATA_V1_SIZE = sizeof(NOTIFYICONDATAW_V1) + + +def get_NOTIFYICONDATAW(*largs): + notify_data = NOTIFYICONDATAW(*largs) + + # get shell32 version to find correct NOTIFYICONDATAW size + DllGetVersion = windll.Shell32.DllGetVersion + DllGetVersion.argtypes = [POINTER(DLLVERSIONINFO)] + DllGetVersion.restype = HRESULT + + version = get_DLLVERSIONINFO() + if DllGetVersion(version): + raise Exception('Cannot get Windows version numbers.') + v = MAKEDLLVERULL(version.dwMajorVersion, version.dwMinorVersion, + version.dwBuildNumber, version.dwPlatformID) + + # from the version info find the NOTIFYICONDATA size + if v >= MAKEDLLVERULL(6, 0, 6, 0): + notify_data.cbSize = sizeof(NOTIFYICONDATAW) + elif v >= MAKEDLLVERULL(6, 0, 0, 0): + notify_data.cbSize = NOTIFYICONDATA_V3_SIZE + elif v >= MAKEDLLVERULL(5, 0, 0, 0): + notify_data.cbSize = NOTIFYICONDATA_V2_SIZE + else: + notify_data.cbSize = NOTIFYICONDATA_V1_SIZE + return notify_data + + +CreateWindowExW = windll.User32.CreateWindowExW +CreateWindowExW.argtypes = [DWORD, ATOM, LPCWSTR, DWORD, INT, INT, INT, INT, + HWND, HMENU, HINSTANCE, LPVOID] +CreateWindowExW.restype = HWND + +GetModuleHandleW = windll.Kernel32.GetModuleHandleW +GetModuleHandleW.argtypes = [LPCWSTR] +GetModuleHandleW.restype = HMODULE + +WindowProc = WINFUNCTYPE(LRESULT, HWND, UINT, WPARAM, LPARAM) +DefWindowProcW = windll.User32.DefWindowProcW +DefWindowProcW.argtypes = [HWND, UINT, WPARAM, LPARAM] +DefWindowProcW.restype = LRESULT + + +class WNDCLASSEXW(Structure): + _fields_ = [ + ('cbSize', UINT), + ('style', UINT), + ('lpfnWndProc', WindowProc), + ('cbClsExtra', INT), + ('cbWndExtra', INT), + ('hInstance', HINSTANCE), + ('hIcon', HICON), + ('hCursor', HCURSOR), + ('hbrBackground', HBRUSH), + ('lpszMenuName', LPCWSTR), + ('lpszClassName', LPCWSTR), + ('hIconSm', HICON), + ] + + +def get_WNDCLASSEXW(*largs): + wnd_class = WNDCLASSEXW(*largs) + wnd_class.cbSize = sizeof(WNDCLASSEXW) + return wnd_class + + +RegisterClassExW = windll.User32.RegisterClassExW +RegisterClassExW.argtypes = [POINTER(WNDCLASSEXW)] +RegisterClassExW.restype = ATOM + +UpdateWindow = windll.User32.UpdateWindow +UpdateWindow.argtypes = [HWND] +UpdateWindow.restype = BOOL + +LoadImageW = windll.User32.LoadImageW +LoadImageW.argtypes = [HINSTANCE, LPCWSTR, UINT, INT, INT, UINT] +LoadImageW.restype = HANDLE + +Shell_NotifyIconW = windll.Shell32.Shell_NotifyIconW +Shell_NotifyIconW.argtypes = [DWORD, POINTER(NOTIFYICONDATAW)] +Shell_NotifyIconW.restype = BOOL + +DestroyIcon = windll.User32.DestroyIcon +DestroyIcon.argtypes = [HICON] +DestroyIcon.restype = BOOL + +UnregisterClassW = windll.User32.UnregisterClassW +UnregisterClassW.argtypes = [ATOM, HINSTANCE] +UnregisterClassW.restype = BOOL + +DestroyWindow = windll.User32.DestroyWindow +DestroyWindow.argtypes = [HWND] +DestroyWindow.restype = BOOL + +LoadIconW = windll.User32.LoadIconW +LoadIconW.argtypes = [HINSTANCE, LPCWSTR] +LoadIconW.restype = HICON + + +class SYSTEM_POWER_STATUS(Structure): + _fields_ = [ + ('ACLineStatus', BYTE), + ('BatteryFlag', BYTE), + ('BatteryLifePercent', BYTE), + ('Reserved1', BYTE), + ('BatteryLifeTime', DWORD), + ('BatteryFullLifeTime', DWORD), + ] + + +SystemPowerStatusP = POINTER(SYSTEM_POWER_STATUS) + +GetSystemPowerStatus = windll.kernel32.GetSystemPowerStatus +GetSystemPowerStatus.argtypes = [SystemPowerStatusP] +GetSystemPowerStatus.restype = BOOL + + +class GUID_(Structure): + _fields_ = [ + ('Data1', DWORD), + ('Data2', WORD), + ('Data3', WORD), + ('Data4', BYTE * 8) + ] + + def __init__(self, uuid_): + Structure.__init__(self) + self.Data1, self.Data2, self.Data3, self.Data4[0], self.Data4[1], rest\ + = uuid_.fields + for i in range(2, 8): + self.Data4[i] = rest >> (8 - i - 1) * 8 & 0xff + + +_CoTaskMemFree = windll.ole32.CoTaskMemFree +_CoTaskMemFree.restype = None +_CoTaskMemFree.argtypes = [ctypes.c_void_p] + +_SHGetKnownFolderPath = windll.shell32.SHGetKnownFolderPath +_SHGetKnownFolderPath.argtypes = [ + POINTER(GUID_), + DWORD, + HANDLE, + POINTER(ctypes.c_wchar_p) +] + + +class PathNotFoundException(Exception): + pass + + +def get_PATH(folderid): + fid = GUID_(folderid) + pPath = ctypes.c_wchar_p() + S_OK = 0 + Result = _SHGetKnownFolderPath(ctypes.byref(fid), + 0, None, ctypes.byref(pPath)) + if Result != S_OK: + raise PathNotFoundException() + path = pPath.value + _CoTaskMemFree(pPath) + return path diff --git a/sbapp/plyer/platforms/win/notification.py b/sbapp/plyer/platforms/win/notification.py new file mode 100644 index 0000000..bac381b --- /dev/null +++ b/sbapp/plyer/platforms/win/notification.py @@ -0,0 +1,24 @@ +''' +Module of Windows API for plyer.notification. +''' + +from threading import Thread as thread + +from plyer.facades import Notification +from plyer.platforms.win.libs.balloontip import balloon_tip + + +class WindowsNotification(Notification): + ''' + Implementation of Windows notification/balloon API. + ''' + + def _notify(self, **kwargs): + thread(target=balloon_tip, kwargs=kwargs).start() + + +def instance(): + ''' + Instance for facade proxy. + ''' + return WindowsNotification() diff --git a/sbapp/plyer/platforms/win/screenshot.py b/sbapp/plyer/platforms/win/screenshot.py new file mode 100644 index 0000000..b04eab5 --- /dev/null +++ b/sbapp/plyer/platforms/win/screenshot.py @@ -0,0 +1,107 @@ +''' +https://docs.microsoft.com/en-us/windows/desktop/api/wingdi/nf-wingdi-bitblt +https://www.bugs.python.org/issue33656 +''' + +from os import getpid +from os.path import join +from ctypes import windll, c_int, addressof + +from win32gui import GetDesktopWindow, GetWindowDC +from win32api import GetSystemMetrics +from win32ui import CreateDCFromHandle, CreateBitmap +from win32con import ( + SM_CXVIRTUALSCREEN, + SM_CYVIRTUALSCREEN, + SM_XVIRTUALSCREEN, + SM_YVIRTUALSCREEN, + SRCCOPY +) + +from plyer.facades import Screenshot +from plyer.platforms.win.storagepath import WinStoragePath + + +class WinScreenshot(Screenshot): + def __init__(self, file_path=None): + default_path = join( + WinStoragePath().get_pictures_dir(), 'screenshot.bmp' + ) + super().__init__(file_path or default_path) + + def _set_dpi_aware(self, value): + try: + windll.shcore.SetProcessDpiAwareness(value) + except (AttributeError, OSError): + print('Could not set DPI awareness.') + + def _dpi_aware(self): + # make backup of DPI awareness value in case a user does not want + # to use it, otherwise we'll cripple user's runtime + try: + process_handle = windll.kernel32.OpenProcess( + 0, # no permissions + False, # bInheritHandle + getpid() + ) + aware = c_int() + windll.shcore.GetProcessDpiAwareness( + process_handle, + addressof(aware) + ) + finally: + # always close the handle! + windll.kernel32.CloseHandle(process_handle) + return bool(aware) + + def _capture(self): + # make sure the process is DPI aware, otherwise the image + # is only a part of the full monitor content + # (necessary only on Win 8.1+) + old_awareness = self._dpi_aware() + self._set_dpi_aware(True) + + # get width and height of current monitor + width = GetSystemMetrics(SM_CXVIRTUALSCREEN) + height = GetSystemMetrics(SM_CYVIRTUALSCREEN) + + # get 'desktop' window handle + handle_desktop = GetDesktopWindow() + + # get window graphic context handle + handle_context = GetWindowDC(handle_desktop) + + # create device context + dev_ctx = CreateDCFromHandle(handle_context) + + # create destination for original device context + dest_ctx = dev_ctx.CreateCompatibleDC() + + # create bitmap compatible with desktop window device + bmp = CreateBitmap() + bmp.CreateCompatibleBitmap(dev_ctx, width, height) + + # select bitmap into destination device + dest_ctx.SelectObject(bmp) + + # populate selected bitmap in destination device + dest_ctx.BitBlt( + (0, 0), # start point (x, y) + (width, height), # size of rectangle + dev_ctx, # source device + ( + GetSystemMetrics(SM_XVIRTUALSCREEN), + GetSystemMetrics(SM_YVIRTUALSCREEN) + ), # source rectangle, can be different monitor + SRCCOPY # copy directly without filters + ) + + # save bitmap to file + bmp.SaveBitmapFile(dest_ctx, self.file_path) + + # return to the original state + self._set_dpi_aware(old_awareness) + + +def instance(): + return WinScreenshot() diff --git a/sbapp/plyer/platforms/win/storagepath.py b/sbapp/plyer/platforms/win/storagepath.py new file mode 100755 index 0000000..fd905d8 --- /dev/null +++ b/sbapp/plyer/platforms/win/storagepath.py @@ -0,0 +1,53 @@ +''' +Windows Storage Path +-------------------- +''' + +from plyer.facades import StoragePath +from os.path import expanduser +from plyer.platforms.win.libs.win_api_defs import get_PATH +from uuid import UUID + + +class WinStoragePath(StoragePath): + + def _get_home_dir(self): + return expanduser('~') + + def _get_external_storage_dir(self): + ''' + To do. + ''' + return "Method not implemented for current platform." + + def _get_root_dir(self): + folderid = UUID('{F38BF404-1D43-42F2-9305-67DE0B28FC23}') + return get_PATH(folderid) + + def _get_documents_dir(self): + folderid = UUID('{FDD39AD0-238F-46AF-ADB4-6C85480369C7}') + return get_PATH(folderid) + + def _get_downloads_dir(self): + folderid = UUID('{374DE290-123F-4565-9164-39C4925E467B}') + return get_PATH(folderid) + + def _get_videos_dir(self): + folderid = UUID('{18989B1D-99B5-455B-841C-AB7C74E4DDFC}') + return get_PATH(folderid) + + def _get_music_dir(self): + folderid = UUID('{4BD8D571-6D19-48D3-BE97-422220080E43}') + return get_PATH(folderid) + + def _get_pictures_dir(self): + folderid = UUID('{33E28130-4E1E-4676-835A-98395C3BC3BB}') + return get_PATH(folderid) + + def _get_application_dir(self): + folderid = UUID('{905E63B6-C1BF-494E-B29C-65B732D3D21A}') + return get_PATH(folderid) + + +def instance(): + return WinStoragePath() diff --git a/sbapp/plyer/platforms/win/tts.py b/sbapp/plyer/platforms/win/tts.py new file mode 100644 index 0000000..e2539c3 --- /dev/null +++ b/sbapp/plyer/platforms/win/tts.py @@ -0,0 +1,16 @@ +import subprocess +from plyer.facades import TTS +from plyer.utils import whereis_exe + + +class EspeakTextToSpeech(TTS): + ''' Speaks using the espeak program + ''' + def _speak(self, **kwargs): + subprocess.call(["espeak", kwargs.get('message')]) + + +def instance(): + if whereis_exe('espeak.exe'): + return EspeakTextToSpeech() + return TTS() diff --git a/sbapp/plyer/platforms/win/uniqueid.py b/sbapp/plyer/platforms/win/uniqueid.py new file mode 100644 index 0000000..6d42391 --- /dev/null +++ b/sbapp/plyer/platforms/win/uniqueid.py @@ -0,0 +1,36 @@ +''' +Module of Windows API for plyer.uniqueid. +''' + +try: + import _winreg as regedit +except ImportError: + try: + import winreg as regedit + except ImportError: + raise NotImplementedError() + +from plyer.facades import UniqueID + + +class WinUniqueID(UniqueID): + ''' + Implementation of Windows battery API. + ''' + + def _get_uid(self): + # Win XP+, REG QUERY KEY /V VALUE, case-insensitive + handle = regedit.OpenKey( + regedit.HKEY_LOCAL_MACHINE, + r"SOFTWARE\\Microsoft\\Cryptography", 0, + regedit.KEY_READ | regedit.KEY_WOW64_64KEY + ) + value, _ = regedit.QueryValueEx(handle, "MachineGuid") + return value + + +def instance(): + ''' + Instance for facade proxy. + ''' + return WinUniqueID() diff --git a/sbapp/plyer/platforms/win/wifi.py b/sbapp/plyer/platforms/win/wifi.py new file mode 100644 index 0000000..68efb2f --- /dev/null +++ b/sbapp/plyer/platforms/win/wifi.py @@ -0,0 +1,121 @@ +import plyer.platforms.win.libs.wifi_defs as wifi_lib +from plyer.facades import Wifi + + +class WindowWifi(Wifi): + + names = {} + + def _is_enabled(self): + ''' + TODO: Implement this in future + Couldn't find a nice implementation for this although + NetworkInformation class could be used but ctypes doesn't supports + class yet. It should look something like this. + + for item in NetworkInformation.getConnectionProfiles(): + if item.IsWlanConnectionProfile: + adapter_id = item.NetworkAdapter.NetworkAdapterId + for item in NetworkInformation.GetLanIdentifiers(): + if item.NetworkAdapterId == adapter_id: + is_wifi_enabled = True + return True/False + + Returning True for now to make it work. + ''' + return True + + def _get_network_info(self, name): + ''' + Returns all the network information. + ''' + return wifi_lib.get_network_info(name) + + def _start_scanning(self): + ''' + Starts scanning for available Wi-Fi networks and returns the available, + devices. + ''' + if self._is_enabled(): + self.names = wifi_lib.start_scanning() + else: + raise Exception('Wifi not Enabled.') + + def _get_available_wifi(self): + ''' + Returns the name of available networks. + ''' + return wifi_lib.get_available_wifi() + + def _connect(self, network, parameters): + ''' + Expects 2 parameters: + - name/ssid of the network. + - parameters: dict type + - connection_mode: + `https://msdn.microsoft.com/en-us/library/windows/desktop/ + ms706844(v=vs.85).aspx` + + :between range [0, 5] + - wlan_connection_mode_profile, + wlan_connection_mode_temporary_profile, + wlan_connection_mode_discovery_secure, + wlan_connection_mode_discovery_unsecure, + wlan_connection_mode_auto, + wlan_connection_mode_invalid. + - profile: + if wlanConnectionMode = wlan_connection_mode_profile + then profile = ssid + if wlanConnectionMode = wlan_connection_mode_temporary_profile + then profile = XML representation of the profile used for the + connection + if wlanConnectionMode = wlan_connection_mode_discovery_secure + or + wlan_connection_mode_discovery_unsecure + then profile = None + - ssid: optional (as network name and ssid are same) + - bssidList + `https://msdn.microsoft.com/en-us/library/windows/desktop/ + ms705996(v=vs.85).aspx` + - Header + structure that contains the type, version, and, size + information + of an NDIS structure. + - Type: NDSI_OBJECT_TYPE_DEFAULT + - Revision: DOT11_BSSID_LIST_REVISION_1 + - Size: sizeof(DOT11_BSSID_LIST) + - uNumOfEntries + The number of entries in this structure. + - uTotalNumOfEntries + The total number of entries supported. + - BSSIDs + `https://msdn.microsoft.com/en-us/library/windows/desktop/ + bb427397(v=vs.85).aspx` + A list of BSS identifiers. + - bssType + `https://msdn.microsoft.com/en-us/library/windows/desktop/ + ms706001(v=vs.85).aspx` + Constants: + dot11_BSS_type_infrastructure = 1, + dot11_BSS_type_independent = 2, + dot11_BSS_type_any = 3 + - flags + Constant: WLAN_CONNECTION_HIDDEN_NETWORK value: 0x00000001 + Constant: WLAN_CONNECTION_ADHOC_JOIN_ONLY value: 0x00000002 + Constant: WLAN_CONNECTION_IGNORE_PRIVACY_BIT value: 0x00000004 + Constant: WLAN_CONNECTION_EAPOL_PASSTHROUGH value: 0x00000008 + - password + ''' + wifi_lib.connect(network, parameters) + return + + def _disconnect(self): + ''' + Disconnect from network. + ''' + wifi_lib.disconnect() + return + + +def instance(): + return WindowWifi() diff --git a/sbapp/plyer/tests/__init__.py b/sbapp/plyer/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sbapp/plyer/tests/common.py b/sbapp/plyer/tests/common.py new file mode 100644 index 0000000..7436db8 --- /dev/null +++ b/sbapp/plyer/tests/common.py @@ -0,0 +1,76 @@ +''' +Common objects for testing +========================== + +* :class:`PlatformTest` - used as a decorator, allows running a test function + only on a specific platform (see `plyer.utils.platform`). +* :func:`platform_import` - manual import of a platform specific class instead + of using `plyer.facades.*` proxies. +''' + +import traceback +from os import sep +from os.path import normpath, splitdrive +from plyer.utils import platform as plyer_platform + + +class PlatformTest: + ''' + Class for the @PlatformTest decorator to prevent running tests + calling platform dependent API on different platforms. + ''' + + def __init__(self, platform): + self.platform = platform + + def __call__(self, func): + platform = self.platform + + if platform != plyer_platform: + print("Skipping test '{}' - not on '{}'".format( + func.__name__, platform + )) + func = self.eat + return func + + @staticmethod + def eat(*args, **kwargs): + ''' + Simply eat all positional and keyword arguments + and return None as an empty function. + ''' + + +def platform_import(platform, module_name, whereis_exe=None): + ''' + Import platform API directly instead of through Proxy. + ''' + + try: + module = 'plyer.platforms.{}.{}'.format( + platform, module_name + ) + mod = __import__(module, fromlist='.') + + except ImportError as exc: + print(vars(exc)) + traceback.print_exc() + + if whereis_exe: + mod.whereis_exe = whereis_exe + return mod + + +def splitpath(path): + ''' + Split string path into a list of folders (+ file if available). + ''' + if path[0] == sep and path[1] != sep: + path = path[1:] + path = normpath(path).split(sep) + else: + drive, path = splitdrive(path) + if path[0] == sep and path[1] != sep: + path = path[1:] + path = [drive, ] + normpath(path).split(sep) + return path diff --git a/sbapp/plyer/tests/images/kivy32.ico b/sbapp/plyer/tests/images/kivy32.ico new file mode 100644 index 0000000000000000000000000000000000000000..2010bf3c335b84fbd7d1eb58cc4fa2fe6e626cdf GIT binary patch literal 4286 zcmchaO-NKx6vyvtk`@I@L93$C>TZ^no=6>Z8|h2l!Aj0l&Frbbc_gw!HK z`Tz-9M$*b$v>DvyvcZi(IWyG=BS^RZdA<9%b9wL0o95Y*<9qMiJLmU5=iGZ88Po6+ z3^Mx7<{KE>&lo!ngNr3$XXCNR{a7qAo7Cf<$YRmR zEPT^logL>eXAg|#`gPT{w47+u6S4VJGV!xFrSnRbh)3UahC9wfKwS+YwC2N22a(s? zN<61===jjs7z{!~wf)iBT2H2s?+>TgUj0n-W%w@%9oZF`S^Ic%6B ze>1Ea4Pd^Z%H>N&zjL{D zOM_gGV)2(PpYkUSmDU6JGlGx$PpN!bYbz_C%QQTneP(H)dgSxDG7W+)df}!4d;O!t zpS0r>L)M`3H#UkEUu9R)AoHmgPJCH|Tz~SDQGcA+ZW_RREb@nzn{IsKQ0~+d!4>zZ z$q9aKc!)2&e`lOU@|hs`bXR);=9t7!x$!AS+IK3S>Y;v4y!Z88G<1!Q-sVrA%orxd z=#Kn*1s`_}zJfe1Sb|Ms!biEgvE?`p&jm2cz7JsDoIICUGMmN(zeYCIY);DEctk&f zGN04Fsi5Rpu zDOMilc;=a7G-r-_@6QOmd^QNfOE$97Grveu|qhV o>tVaHe=V?x@9Quv?a|Enfvg#W;!#|R|C_NpJgsqFsONP30oI2A_y7O^ literal 0 HcmV?d00001 diff --git a/sbapp/plyer/tests/test_audio.py b/sbapp/plyer/tests/test_audio.py new file mode 100644 index 0000000..e811891 --- /dev/null +++ b/sbapp/plyer/tests/test_audio.py @@ -0,0 +1,110 @@ +''' +TestAudio +========= + +Tested platforms: + +* macOS +* Windows + +.. versionadded:: 1.4.0 +''' + +import unittest +import time + +from os import mkdir, remove, environ +from os.path import join, expanduser, exists +from plyer.tests.common import platform_import, PlatformTest + + +class TestAudio(unittest.TestCase): + ''' + TestCase for plyer.audio. + + .. versionadded:: 1.4.0 + ''' + + @PlatformTest('macosx') + def test_audio_macosx(self): + ''' + Test macOS audio start, stop and play + + .. versionadded:: 1.4.0 + ''' + + path = join(expanduser('~'), 'Music') + if not exists(path): + mkdir(path) + + audio = platform_import( + platform='macosx', + module_name='audio', + ) + + self.assertIn('OSXAudio', dir(audio)) + audio = audio.instance() + self.assertIn('OSXAudio', str(audio)) + + self.assertFalse(exists(audio.file_path)) + self.assertIsNone(audio.start()) + time.sleep(0.5) + self.assertIsNone(audio.stop()) + self.assertIsNone(audio.play()) + time.sleep(0.5) + self.assertIsNone(audio.stop()) + + audio.file_path = audio.file_path.replace( + 'file://', '' + ) + + self.assertTrue(exists(audio.file_path)) + + remove(audio.file_path) + + @PlatformTest('win') + def test_audio_win(self): + ''' + Test Windows audio start, stop and play + + .. versionadded:: 1.4.0 + ''' + + if environ.get('APPVEYOR'): + # Appveyor has no recording device installed + # therefore the test will 100% fail + # + # error_code: 328 + # message: + # 'No wave device is installed that can record files in the current + # format. To install a wave device, go to Control Panel, click P') + return + + path = join(environ['USERPROFILE'], 'Music') + if not exists(path): + mkdir(path) + + audio = platform_import( + platform='win', + module_name='audio', + ) + + self.assertIn('WinAudio', dir(audio)) + audio = audio.instance() + self.assertIn('WinAudio', str(audio)) + + self.assertFalse(exists(audio.file_path)) + self.assertIsNone(audio.start()) + time.sleep(0.5) + self.assertIsNone(audio.stop()) + self.assertIsNone(audio.play()) + time.sleep(0.5) + self.assertIsNone(audio.stop()) + + self.assertTrue(exists(audio.file_path)) + + remove(audio.file_path) + + +if __name__ == '__main__': + unittest.main() diff --git a/sbapp/plyer/tests/test_battery.py b/sbapp/plyer/tests/test_battery.py new file mode 100644 index 0000000..b44d504 --- /dev/null +++ b/sbapp/plyer/tests/test_battery.py @@ -0,0 +1,413 @@ +''' +TestBattery +=========== + +Tested platforms: + +* Windows +* Linux - upower, kernel sysclass +* macOS - ioreg +''' + +import unittest +from io import BytesIO +from os.path import join +from textwrap import dedent +from mock import patch, Mock + +from plyer.tests.common import PlatformTest, platform_import + + +class MockedKernelSysclass: + ''' + Mocked object used instead of Linux's sysclass for power_supply + battery uevent. + ''' + + @property + def path(self): + ''' + Mocked path to Linux kernel sysclass. + ''' + return join('/sys', 'class', 'power_supply', 'BAT0') + + @property + def charging(self): + ''' + Mocked battery charging status. + ''' + return u'Discharging' + + @property + def percentage(self): + ''' + Mocked battery charge percentage. + ''' + return 89.0 + + @property + def full(self): + ''' + Mocked full battery charge. + ''' + return 4764000 + + @property + def now(self): + ''' + Calculated current mocked battery charge. + ''' + return self.percentage * self.full / 100.0 + + @property + def uevent(self): + ''' + Mocked /sys/class/power_supply/BAT0 file. + ''' + return BytesIO(dedent(b'''\ + POWER_SUPPLY_NAME=BAT0 + POWER_SUPPLY_STATUS={} + POWER_SUPPLY_PRESENT=1 + POWER_SUPPLY_TECHNOLOGY=Li-ion + POWER_SUPPLY_CYCLE_COUNT=0 + POWER_SUPPLY_VOLTAGE_MIN_DESIGN=10800000 + POWER_SUPPLY_VOLTAGE_NOW=12074000 + POWER_SUPPLY_CURRENT_NOW=1584000 + POWER_SUPPLY_CHARGE_FULL_DESIGN=5800000 + POWER_SUPPLY_CHARGE_FULL={} + POWER_SUPPLY_CHARGE_NOW={} + POWER_SUPPLY_CAPACITY={} + POWER_SUPPLY_CAPACITY_LEVEL=Normal + POWER_SUPPLY_MODEL_NAME=1005HA + POWER_SUPPLY_MANUFACTURER=ASUS + POWER_SUPPLY_SERIAL_NUMBER=0 + '''.decode('utf-8').format( + self.charging, self.full, + self.now, int(self.percentage) + )).encode('utf-8')) + + +class MockedUPower: + ''' + Mocked object used instead of 'upower' binary in the Linux specific API + plyer.platforms.linux.battery. The same output structure is tested for + the range of . + + .. note:: Extend the object with another data sample if it does not match. + ''' + + min_version = '0.99.4' + max_version = '0.99.4' + + values = { + u'Device': u'/org/freedesktop/UPower/devices/battery_BAT0', + u'native-path': u'BAT0', + u'vendor': u'ASUS', + u'model': u'1005HA', + u'power supply': u'yes', + u'updated': u'Thu 05 Jul 2018 23:15:01 PM CEST', + u'has history': u'yes', + u'has statistics': u'yes', + u'battery': { + u'present': u'yes', + u'rechargeable': u'yes', + u'state': u'discharging', + u'warning-level': u'none', + u'energy': u'48,708 Wh', + u'energy-empty': u'0 Wh', + u'energy-full': u'54,216 Wh', + u'energy-full-design': u'62,64 Wh', + u'energy-rate': u'7,722 W', + u'voltage': u'11,916 V', + u'time to empty': u'6,3 hours', + u'percentage': u'89%', + u'capacity': u'86,5517%', + u'technology': u'lithium-ion', + u'icon-name': u"'battery-full-symbolic" + }, + u'History (charge)': u'1530959637 89,000 discharging', + u'History (rate)': u'1530958556 7,474 discharging' + } + + data = str( + ' native-path: {native-path}\n' + ' vendor: {vendor}\n' + ' model: {model}\n' + ' power supply: {power supply}\n' + ' updated: {updated}\n' + ' has history: {has history}\n' + ' has statistics: {has statistics}\n' + ' battery\n' + ' present: {battery[present]}\n' + ' rechargeable: {battery[rechargeable]}\n' + ' state: {battery[state]}\n' + ' warning-level: {battery[warning-level]}\n' + ' energy: {battery[energy]}\n' + ' energy-empty: {battery[energy-empty]}\n' + ' energy-full: {battery[energy-full]}\n' + ' energy-full-design: {battery[energy-full-design]}\n' + ' energy-rate: {battery[energy-rate]}\n' + ' voltage: {battery[voltage]}\n' + ' time to empty: {battery[time to empty]}\n' + ' percentage: {battery[percentage]}\n' + ' capacity: {battery[capacity]}\n' + ' technology: {battery[technology]}\n' + ' icon-name: {battery[icon-name]}\n' + ' History (charge):\n' + ' {History (charge)}\n' + ' History (rate):\n' + ' {History (rate)}\n' + ).format(**values).encode('utf-8') + # LinuxBattery calls decode() + + def __init__(self, *args, **kwargs): + # only to ignore all args, kwargs + pass + + @staticmethod + def communicate(): + ''' + Mock Popen.communicate, so that 'upower' isn't used. + ''' + return (MockedUPower.data, ) + + @staticmethod + def whereis_exe(binary): + ''' + Mock whereis_exe, so that it looks like + Linux UPower binary is present on the system. + ''' + return binary == 'upower' + + @staticmethod + def charging(): + ''' + Return charging bool from mocked data. + ''' + return MockedUPower.values['battery']['state'] == 'charging' + + @staticmethod + def percentage(): + ''' + Return percentage from mocked data. + ''' + percentage = MockedUPower.values['battery']['percentage'][:-1] + return float(percentage.replace(',', '.')) + + +class MockedIOReg: + ''' + Mocked object used instead of Apple's ioreg. + ''' + values = { + "MaxCapacity": "5023", + "CurrentCapacity": "4222", + "IsCharging": "No" + } + + output = dedent( + """+-o AppleSmartBattery + {{ + "TimeRemaining" = 585 + "AvgTimeToEmpty" = 585 + "InstantTimeToEmpty" = 761 + "ExternalChargeCapable" = Yes + "FullPathUpdated" = 1541845134 + "CellVoltage" = (4109,4118,4099,0) + "PermanentFailureStatus" = 0 + "BatteryInvalidWakeSeconds" = 30 + "AdapterInfo" = 0 + "MaxCapacity" = {MaxCapacity} + "Voltage" = 12326 + "DesignCycleCount70" = 13 + "Manufacturer" = "SWD" + "Location" = 0 + "CurrentCapacity" = {CurrentCapacity} + "LegacyBatteryInfo" = {{"Amperage"=18446744073709551183,"Flags"=4,\ + "Capacity"=5023,"Current"=4222,"Voltage"=12326,"Cycle Count"=40}} + "FirmwareSerialNumber" = 1 + "BatteryInstalled" = Yes + "PackReserve" = 117 + "CycleCount" = 40 + "DesignCapacity" = 5088 + "OperationStatus" = 58435 + "ManufactureDate" = 19700 + "AvgTimeToFull" = 65535 + "BatterySerialNumber" = "1234567890ABCDEFGH" + "BootPathUpdated" = 1541839734 + "PostDischargeWaitSeconds" = 120 + "Temperature" = 3038 + "UserVisiblePathUpdated" = 1541845194 + "InstantAmperage" = 18446744073709551249 + "ManufacturerData" = <000000000> + "FullyCharged" = No + "MaxErr" = 1 + "DeviceName" = "bq20z451" + "IOGeneralInterest" = "IOCommand is not serializable" + "Amperage" = 18446744073709551183 + "IsCharging" = {IsCharging} + "DesignCycleCount9C" = 1000 + "PostChargeWaitSeconds" = 120 + "ExternalConnected" = No + }}""" + ).format(**values).encode('utf-8') + + def __init__(self, *args, **kwargs): + # only to ignore all args, kwargs + pass + + @staticmethod + def communicate(): + ''' + Mock Popen.communicate, so that 'ioreg' isn't used. + ''' + return (MockedIOReg.output, ) + + @staticmethod + def whereis_exe(binary): + ''' + Mock whereis_exe, so that it looks like + macOS ioreg binary is present on the system. + ''' + return binary == 'ioreg' + + @staticmethod + def charging(): + ''' + Return charging bool from mocked data. + ''' + return MockedIOReg.values['IsCharging'] == 'Yes' + + @staticmethod + def percentage(): + ''' + Return percentage from mocked data. + ''' + current_capacity = int(MockedIOReg.values['CurrentCapacity']) + max_capacity = int(MockedIOReg.values['MaxCapacity']) + percentage = 100.0 * current_capacity / max_capacity + + return percentage + + +class TestBattery(unittest.TestCase): + ''' + TestCase for plyer.battery. + ''' + + def test_battery_linux_upower(self): + ''' + Test mocked Linux UPower for plyer.battery. + ''' + battery = platform_import( + platform='linux', + module_name='battery', + whereis_exe=MockedUPower.whereis_exe + ) + battery.Popen = MockedUPower + battery = battery.instance() + + self.assertEqual( + battery.status, { + 'isCharging': MockedUPower.charging(), + 'percentage': MockedUPower.percentage() + } + ) + + def test_battery_linux_kernel(self): + ''' + Test mocked Linux kernel sysclass for plyer.battery. + ''' + + def false(*args, **kwargs): + return False + + sysclass = MockedKernelSysclass() + + with patch(target='os.path.exists') as bat_path: + # first call to trigger exists() call + platform_import( + platform='linux', + module_name='battery', + whereis_exe=false + ).instance() + bat_path.assert_called_once_with(sysclass.path) + + # exists() checked with sysclass path + # set mock to proceed with this branch + bat_path.return_value = True + + battery = platform_import( + platform='linux', + module_name='battery', + whereis_exe=false + ).instance() + + stub = Mock(return_value=sysclass.uevent) + target = 'builtins.open' + + with patch(target=target, new=stub): + self.assertEqual( + battery.status, { + 'isCharging': sysclass.charging == 'Charging', + 'percentage': sysclass.percentage + } + ) + + @PlatformTest('win') + def test_battery_win(self): + ''' + Test Windows API for plyer.battery. + ''' + battery = platform_import( + platform='win', + module_name='battery' + ).instance() + for key in ('isCharging', 'percentage'): + self.assertIn(key, battery.status) + self.assertIsNotNone(battery.status[key]) + + def test_battery_macosx(self): + ''' + Test macOS IOReg for plyer.battery. + ''' + battery = platform_import( + platform='macosx', + module_name='battery', + whereis_exe=MockedIOReg.whereis_exe + ) + + battery.Popen = MockedIOReg + self.assertIn('OSXBattery', dir(battery)) + battery = battery.instance() + self.assertIn('OSXBattery', str(battery)) + + self.assertEqual( + battery.status, { + 'isCharging': MockedIOReg.charging(), + 'percentage': MockedIOReg.percentage() + } + ) + + def test_battery_macosx_instance(self): + ''' + Test macOS instance for plyer.battery + ''' + + def no_exe(*args, **kwargs): + return + + battery = platform_import( + platform='macosx', + module_name='battery', + whereis_exe=no_exe + ) + + battery = battery.instance() + self.assertNotIn('OSXBattery', str(battery)) + self.assertIn('Battery', str(battery)) + + +if __name__ == '__main__': + unittest.main() diff --git a/sbapp/plyer/tests/test_bluetooth.py b/sbapp/plyer/tests/test_bluetooth.py new file mode 100644 index 0000000..8997c4d --- /dev/null +++ b/sbapp/plyer/tests/test_bluetooth.py @@ -0,0 +1,144 @@ +''' +TestBluetooth +============= + +Tested platforms: + +* macOS - system_profiler +''' + +import unittest + +from plyer.tests.common import platform_import +from textwrap import dedent + + +class MockedSystemProfiler: + ''' + Mocked object used instead of Apple's system_profiler + ''' + value = "On" + output = dedent( + """Bluetooth: + + Apple Bluetooth Software Version: 6.0.7f11 + Hardware, Features, and Settings: + Address: AA-00-BB-11-CC-22 + Bluetooth Low Energy Supported: Yes + Handoff Supported: Yes + Instant Hot Spot Supported: Yes + Manufacturer: Broadcom + Transport: UART + Chipset: 1234 + Firmware Version: v00 c0000 + Bluetooth Power: {} + Auto Seek Pointing: On + Remote wake: On + Vendor ID: 0x0000 + Product ID: 0x0000 + HCI Version: (0x0) + HCI Revision: 0x0000 + LMP Version: (0x0) + LMP Subversion: 0x0000 + Auto Seek Keyboard: On + Devices (Paired, Configured, etc.): + iPhone: + Address: AA-00-BB-11-CC-22 + Major Type: Miscellaneous + Minor Type: Unknown + Services: + Paired: No + Configured: Yes + Connected: No + Class of Device: 0x00 0x00 0x0000 + Services: + Bluetooth File Transfer: + Folder other devices can browse: ~/Public + When receiving items: Accept all without warning + State: Disabled + Bluetooth File Exchange: + Folder for accepted items: ~/Downloads + When other items are accepted: Save to location + When receiving items: Accept all without warning + State: Disabled + Bluetooth Internet Sharing: + State: Disabled + Incoming Serial Ports: + Bluetooth-Incoming-Port: + RFCOMM Channel: 3 + Requires Authentication: No""" + ).format(value).encode('utf-8') + + def __init__(self, *args, **kwargs): + # only to ignore all args, kwargs + pass + + @staticmethod + def communicate(): + ''' + Mock Popen.communicate, so that 'system_profiler' + isn't used. + ''' + return (MockedSystemProfiler.output, ) + + @staticmethod + def whereis_exe(binary): + ''' + Mock whereis_exe, so that it looks like + macOS system_profiler binary is present on the system. + ''' + return binary == 'system_profiler' + + @staticmethod + def get_info(): + ''' + Return current bluetooth status from mocked output. + ''' + return MockedSystemProfiler.value + + +class TestBluetooth(unittest.TestCase): + ''' + TestCase for plyer.bluetooth. + ''' + + def test_bluetooth_macosx(self): + ''' + Test macOS system_profiler for plyer.bluetooth. + ''' + bluetooth = platform_import( + platform='macosx', + module_name='bluetooth', + whereis_exe=MockedSystemProfiler.whereis_exe + ) + + bluetooth.Popen = MockedSystemProfiler + self.assertIn('OSXBluetooth', dir(bluetooth)) + bluetooth = bluetooth.instance() + self.assertIn('OSXBluetooth', str(bluetooth)) + + self.assertEqual( + bluetooth.info, MockedSystemProfiler.get_info() + ) + + def test_bluetooth_macosx_instance(self): + ''' + Test macOS instance for plyer.bluetooth. + ''' + + def no_exe(*args, **kwargs): + return + + bluetooth = platform_import( + platform='macosx', + module_name='bluetooth', + whereis_exe=no_exe + ) + + bluetooth = bluetooth.instance() + self.assertNotIn('OSXBluetooth', str(bluetooth)) + self.assertIn('Bluetooth', str(bluetooth)) + + +if __name__ == '__main__': + unittest.main() diff --git a/sbapp/plyer/tests/test_cpu.py b/sbapp/plyer/tests/test_cpu.py new file mode 100644 index 0000000..8882522 --- /dev/null +++ b/sbapp/plyer/tests/test_cpu.py @@ -0,0 +1,262 @@ +''' +TestCPU +======= + +Tested platforms: + +* Windows +* Linux - nproc +''' + +import unittest +from os import environ +from os.path import join +from mock import patch, Mock +from textwrap import dedent + +from plyer.tests.common import PlatformTest, platform_import, splitpath + + +class MockedKernelCPU: + def __init__(self, *args, **kwargs): + self.fname = args[0] if args else '' + self.cpu_path = join('/sys', 'devices', 'system', 'cpu') + self.cores = 16 + self.indicies = 4 + + def __enter__(self, *args): + file_value = None + cpu_path = self.cpu_path + spath = splitpath(self.fname) + + if self.fname == join(cpu_path, 'present'): + file_value = Mock() + file_value.read.return_value = self.present + elif spath[5] == 'cache' and spath[7] == 'level': + file_value = Mock() + # force bytes, because reading files as bytes + file_value.read.return_value = str( + self.index_types[spath[4]][spath[6]][spath[7]] + ).encode('utf-8') + return file_value + + def __exit__(self, *args): + pass + + @property + def present(self): + rng = list(range(self.cores)) + start = str(rng[0]) + end = str(rng[-1]) + if start == end: # cores == 1 --> b'0' + value = str(start) + else: # cores > 1 --> b'0-n' + value = str('-'.join([start, end])) + return value.encode('utf-8') + + @property + def listdir(self): + return ['index{}'.format(i) for i in range(self.indicies)] + + @property + def index_types(self): + # assign L1 to index0-1, L2 to 2, L3 to 3 + types = {0: 1, 1: 1, 2: 2, 3: 3} + + return { + 'cpu{}'.format(c): { + 'index{}'.format(i): { + 'level': types[i] + } + for i in range(self.indicies) + } + for c in range(self.cores) + } + + +class MockedNProc: + ''' + Mocked object used instead of 'nproc' binary in the Linux specific API + plyer.platforms.linux.cpu. The same output structure is tested for + the range of . + + .. note:: Extend the object with another data sample if it does not match. + ''' + + min_version = '8.21' + max_version = '8.21' + logical_cores = 99 + + def __init__(self, *args, **kwargs): + # only to ignore all args, kwargs + pass + + @staticmethod + def communicate(): + ''' + Mock Popen.communicate, so that 'nproc' isn't used. + ''' + return (str(MockedNProc.logical_cores).encode('utf-8'), ) + + @staticmethod + def whereis_exe(binary): + ''' + Mock whereis_exe, so that it looks like + Linux NProc binary is present on the system. + ''' + return binary == 'nproc' + + @staticmethod + def logical(): + ''' + Return percentage from mocked data. + ''' + return int(MockedNProc.logical_cores) + + +class MockedProcinfo: + # docs: + # https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git + # /tree/arch/x86/kernel/cpu/proc.c + sockets = 1 # physical id + physical = 2 # core id + threads_per_core = 2 # Intel specs document for i7-4500U + logical = physical * threads_per_core # processor + + def __init__(self, *args, **kwargs): + self.fname = args[0] if args else '' + + self.output = [] + __step = 0 # 0,1,0,1 -> 0,0,1,1 + for soc in range(self.sockets): + for log in range(self.logical): + if log != 0 and not log % self.physical: + __step += 1 + self.output.append((dedent( + '''\ + processor\t: {logical} + vendor_id\t: GenuineIntel + cpu family\t: 6 + model\t\t: 69 + model name\t: Intel(R) Core(TM) i7-4500U CPU @ 1.80GHz + stepping\t: 1 + microcode\t: 0x17 + cpu MHz\t\t: 774.000 + cache size\t: 4096 KB + physical id\t: {socket} + siblings\t: 4 + core id\t\t: {physical} + cpu cores\t: {threads_per_core} + apicid\t\t: {logical} + initial apicid\t: 0 + fpu\t\t: yes + fpu_exception\t: yes + cpuid level\t: 13 + wp\t\t: yes + flags\t\t: fpu vme de pse tsc msr pae mce cx8 ... + bogomips\t: 3591.40 + clflush size\t: 64 + cache_alignment\t: 64 + address sizes\t: 39 bits physical, 48 bits virtual + power management: + \n''' + )).format(**{ + 'socket': soc, + 'physical': __step, + 'logical': log, + 'threads_per_core': self.threads_per_core + })) + self.output = ''.join(self.output).encode('utf-8') + + def __enter__(self, *args): + file_value = None + + if self.fname == '/proc/cpuinfo': + file_value = Mock() + file_value.readlines.return_value = self.output.split( + '\n'.encode('utf-8') + ) + return file_value + + def __exit__(self, *args): + pass + + +class TestCPU(unittest.TestCase): + ''' + TestCase for plyer.cpu. + ''' + + def test_cpu_linux_physical(self): + cpu = platform_import( + platform='linux', + module_name='cpu', + whereis_exe=lambda b: b == 'nproc' + ).instance() + + stub = MockedProcinfo + target = 'builtins.open' + + with patch(target=target, new=stub): + sb = stub() + self.assertEqual( + cpu.physical, sb.physical + ) + + def test_cpu_linux_logical(self): + ''' + Test mocked Linux NProc for plyer.cpu. + ''' + cpu = platform_import( + platform='linux', + module_name='cpu', + whereis_exe=MockedNProc.whereis_exe + ) + cpu.Popen = MockedNProc + cpu = cpu.instance() + + self.assertEqual( + cpu.logical, MockedNProc.logical() + ) + + @PlatformTest('linux') + def test_cpu_linux_cache(self): + cpu = platform_import( + platform='linux', + module_name='cpu', + whereis_exe=lambda b: b == 'nproc' + ).instance() + + stub = MockedKernelCPU + target = 'builtins.open' + sub_target = 'plyer.platforms.linux.cpu.listdir' + + with patch(target=target, new=stub): + with patch(target=sub_target, return_value=stub().listdir): + sb = stub() + self.assertEqual( + cpu.cache, { + 'L1': sb.cores * 2, + 'L2': sb.cores, + 'L3': sb.cores + } + ) + + @PlatformTest('win') + def test_cpu_win_logical(self): + cpu = platform_import( + platform='win', + module_name='cpu' + ) + + cpu = cpu.instance() + self.assertEqual( + cpu.logical, + # https://docs.microsoft.com/en-us/previous-versions/ + # windows/it-pro/windows-xp/bb490954(v=technet.10) + int(environ['NUMBER_OF_PROCESSORS']) + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/sbapp/plyer/tests/test_devicename.py b/sbapp/plyer/tests/test_devicename.py new file mode 100644 index 0000000..cbb0fc4 --- /dev/null +++ b/sbapp/plyer/tests/test_devicename.py @@ -0,0 +1,77 @@ +''' +TestDeviceName +============ + +Tested platforms: + +* Windows +''' + +import unittest +from mock import patch +from plyer.tests.common import PlatformTest, platform_import +import socket + + +class TestDeviceName(unittest.TestCase): + ''' + TestCase for plyer.devicename. + ''' + + @PlatformTest('win') + def test_devicename_win(self): + ''' + Test Windows API for plyer.devicename. + ''' + devicename = platform_import(platform='win', + module_name='devicename' + ) + devicename_instance = devicename.instance() + + with patch.object(socket, + 'gethostname', + return_value='mocked_windows_hostname' + ) as _: + + evaluated_device_name = devicename_instance.device_name + self.assertEqual(evaluated_device_name, 'mocked_windows_hostname') + + @PlatformTest('linux') + def test_devicename_linux(self): + ''' + Test Linux API for plyer.devicename. + ''' + devicename = platform_import(platform='linux', + module_name='devicename' + ) + devicename_instance = devicename.instance() + + with patch.object(socket, + 'gethostname', + return_value='mocked_linux_hostname' + ) as _: + + evaluated_device_name = devicename_instance.device_name + self.assertEqual(evaluated_device_name, 'mocked_linux_hostname') + + @PlatformTest('macosx') + def test_devicename_macosx(self): + ''' + Test MacOSX API for plyer.devicename. + ''' + devicename = platform_import(platform='macosx', + module_name='devicename' + ) + devicename_instance = devicename.instance() + + with patch.object(socket, + 'gethostname', + return_value='mocked_macosx_hostname' + ) as _: + + evaluated_device_name = devicename_instance.device_name + self.assertEqual(evaluated_device_name, 'mocked_macosx_hostname') + + +if __name__ == '__main__': + unittest.main() diff --git a/sbapp/plyer/tests/test_email.py b/sbapp/plyer/tests/test_email.py new file mode 100644 index 0000000..60aa86f --- /dev/null +++ b/sbapp/plyer/tests/test_email.py @@ -0,0 +1,48 @@ +''' +TestEmail +========= + +Tested platforms: + +* Windows +''' + +import unittest + +from mock import Mock, patch +from plyer.tests.common import PlatformTest, platform_import + + +class TestEmail(unittest.TestCase): + ''' + TestCase for plyer.email. + ''' + + @staticmethod + @PlatformTest('win') + def test_email_win(): + ''' + Test starting Windows email client for plyer.email. + ''' + email = platform_import( + platform='win', + module_name='email' + ) + + try: + test_mailto = 'mailto:recipient?subject=subject&body=text' + with patch(target='os.startfile', new=Mock()) as startfile: + email.instance().send( + recipient='recipient', + subject='subject', + text='text' + ) + startfile.assert_called_once_with(test_mailto) + except WindowsError: + # if WE is raised, email client isn't found, + # but the platform code works correctly + print('Mail client not found!') + + +if __name__ == '__main__': + unittest.main() diff --git a/sbapp/plyer/tests/test_facade.py b/sbapp/plyer/tests/test_facade.py new file mode 100644 index 0000000..b49b15f --- /dev/null +++ b/sbapp/plyer/tests/test_facade.py @@ -0,0 +1,183 @@ +''' +TestFacade +========== + +Tested platforms: + +* Android +* iOS +* Windows +* MacOS +* Linux +''' + +import unittest + +import sys +from types import MethodType + +from mock import Mock, patch + +import plyer + + +def mock_platform_module(mod, platform, cls): + ''' + Create a stub module for a specific platform. This module contains: + + * class inheriting from facade implementing the desired feature + * 'instance' function returning an instance of the implementing class + ''' + + # assemble an instance returned from the instance() function + # which is created from a dynamically created class + # .'> e.g.: + # + stub_inst = Mock( + __module__=mod, + __class__=type( + '{}{}'.format(platform.title(), cls), (object, ), { + '__module__': mod + } + ), + ) + + # manual 'return_value' assign to Mock, so that the instance() call + # can return stub_inst's own instance instead of creating another + # unnecessary Mock object + stub_inst.return_value = stub_inst + + # bind custom function returning the class name to stub_inst instance, + # so that instance().show() call requires 'self' i.e. instance parameter + # for the function to access the instance's class name + stub_inst.show = MethodType(lambda slf: slf, stub_inst) + + stub_mod = Mock(instance=stub_inst) + return stub_mod + + +# dummy pyjnius class to silence the import + config +class DummyJnius: + ''' + Mocked PyJNIus module. + ''' + + def __init__(self, *args, **kwargs): + class JavaClass: + ''' + Mocked PyJNIus JavaClass object. + ''' + + def __init__(self): + self.ANDROID_VERSION = None + self.SDK_INT = 1 + self.mActivity = None + + self.autoclass = lambda *a, **kw: JavaClass() + + +class TestFacade(unittest.TestCase): + ''' + TestCase for plyer.utils.Proxy and plyer.facades. + ''' + + def test_facade_existing_platforms(self): + ''' + Test for returning an object for Android API implementation + from Proxy object using a dynamically generated dummy objects. + ''' + _original = plyer.utils.platform + + for plat in {'android', 'ios', 'win', 'macosx', 'linux'}: + plyer.utils.platform = plat + + if plat == 'android': + # android platform automatically imports jnius + sys.modules['jnius'] = DummyJnius() + + # create stub module with instance func and class + stub_mod = mock_platform_module( + mod='plyer.platforms.{}.dummy'.format(plat), + platform=plyer.utils.platform, + cls='Dummy' + ) + + proxy_cls = plyer.utils.Proxy + target = 'builtins.__import__' + + with patch(target=target, return_value=stub_mod): + dummy = proxy_cls('dummy', stub_mod) + + self.assertEqual( + str(dummy.__class__).split("'")[1], + 'plyer.platforms.{}.dummy.{}Dummy'.format( + plat, plat.title() + ) + ) + self.assertEqual( + str(dummy.show().__class__).split("'")[1], + 'plyer.platforms.{}.dummy.{}Dummy'.format( + plat, plat.title() + ) + ) + + plyer.utils.platform = _original + + def test_facade_unknown(self): + ''' + Test fallback of Proxy to facade if there + is no such requested platform. + ''' + + _original = plyer.utils.platform + plyer.utils.platform = 'unknown' + + # no 'unknown' platform (folder), fallback to facade + class MockedProxy(plyer.utils.Proxy): + ''' + Partially mocked Proxy class, so that we pull the error + from traceback.print_exc to the test and check the calls. + ''' + + # _ensure_obj is called only once, to either + # get the platform object or fall back to facade + # therefore the three self.asserts below will return + # different values + expected_asserts = [True, False, False] + + def _ensure_obj(inst): + # called once, prints to stderr + + # mock stderr because traceback.print_exc uses it + # https://github.com/python/cpython/blob/ + # 16dfca4d829e45f36e71bf43f83226659ce49315/Lib/traceback.py#L99 + sys.stderr = Mock() + + # call the original function to trigger + # ImportError warnings in stderr + super(MockedProxy, inst)._ensure_obj() + + # Traceback (most recent call last): + # File "/app/plyer/utils.py", line 88, in _ensure_obj + # mod = __import__(module, fromlist='.') + # ImportError: No module named unknown.dummy + + # must not use self.assertX + # (has to be checked on the go!) + expected_bool = MockedProxy.expected_asserts.pop(0) + call_count = sys.stderr.write.call_count + assert (call_count == 6) == expected_bool, call_count + + # return stderr to the original state + sys.stderr = sys.__stderr__ + + proxy_cls = MockedProxy + facade = Mock() + dummy = proxy_cls('dummy', facade) + + self.assertEqual(dummy._mock_new_parent, facade) + plyer.utils.platform = _original + + +if __name__ == '__main__': + unittest.main() diff --git a/sbapp/plyer/tests/test_notification.py b/sbapp/plyer/tests/test_notification.py new file mode 100644 index 0000000..7177ad7 --- /dev/null +++ b/sbapp/plyer/tests/test_notification.py @@ -0,0 +1,209 @@ +''' +TestNotification +================ + +Tested platforms: + +* Windows +* Linux - notify-send, dbus +''' + +import unittest +import sys + +from time import sleep +from os.path import dirname, abspath, join + +from mock import Mock, patch +from plyer.tests.common import PlatformTest, platform_import + + +class MockedNotifySend: + ''' + Mocked object used instead of the console-like calling + of notify-send binary with parameters. + ''' + @staticmethod + def whereis_exe(binary): + ''' + Mock whereis_exe, so that it looks like + Linux notify-send binary is present on the system. + ''' + return binary == 'notify-send' + + @staticmethod + def call(args): + ''' + Mocked subprocess.call to check console parameters. + ''' + assert len(args) >= 3 + assert TestNotification.data['title'] in args + assert TestNotification.data['message'] in args + + @staticmethod + def warn(msg): + ''' + Mocked warnings.warn, so that we check the custom ImportError message. + ''' + assert 'dbus package is not installed' in msg + + +class TestNotification(unittest.TestCase): + ''' + TestCase for plyer.notification. + ''' + + data = { + 'title': 'title', + 'message': 'My Message\nis multiline', + 'app_name': 'Plyer Test', + 'app_icon': join( + dirname(abspath(__file__)), + 'images', 'kivy32.ico' + ), + 'timeout': 0.7 + } + + def show_notification(self, instance): + ''' + Call notify() from platform specific instance with sample data. + ''' + instance.notify(**self.data) + + @PlatformTest('win') + def test_notification_windows(self): + ''' + Test Windows API for plyer.notification. + ''' + import ctypes + from ctypes import ( + WINFUNCTYPE, POINTER, + create_unicode_buffer, + c_bool, c_int + ) + notif = platform_import( + platform='win', + module_name='notification' + ).instance() + enum_windows = ctypes.windll.user32.EnumWindows + get_class_name = ctypes.windll.user32.GetClassNameW + + # loop over windows and get refs to + # the opened plyer notifications + clsnames = [] + + def fetch_class(hwnd, *args): + ''' + EnumWindowsProc callback for EnumWindows. + ''' + buff = create_unicode_buffer(50) + get_class_name(hwnd, buff, 50) + + if 'Plyer' in buff.value: + clsnames.append(buff.value) + + # ensure it's not an empty facade + self.assertIn('WindowsNotification', str(notif)) + + # create enum function for EnumWindows + enum_windows_proc = WINFUNCTYPE( + # returns + c_bool, + + # input params: hwnd, lParam + POINTER(c_int), POINTER(c_int) + ) + + for i in range(3): + self.show_notification(notif) + + # the balloon needs some time to became visible in WinAPI + sleep(0.2) + + # fetch window class names + enum_windows( + # enum & params + enum_windows_proc(fetch_class), None + ) + + # 3 active balloons at the same time, + # class_name is incremented - see WindowsBalloonTip + self.assertEqual(len(clsnames), i + 1) + self.assertIn('PlyerTaskbar' + str(i), clsnames) + clsnames = [] + + @PlatformTest('linux') + def test_notification_dbus(self): + ''' + Test mocked Linux DBus for plyer.notification. + ''' + notif = platform_import( + platform='linux', + module_name='notification' + ) + self.assertIn('NotifyDbus', dir(notif)) + + # (3) mocked Interface called from dbus + interface = Mock() + interface.side_effect = (interface, ) + + # (2) mocked SessionBus called from dbus + session_bus = Mock() + session_bus.side_effect = (session_bus, ) + + # (1) mocked dbus for import + dbus = Mock(SessionBus=session_bus, Interface=interface) + + # inject the mocked module + self.assertNotIn('dbus', sys.modules) + sys.modules['dbus'] = dbus + + try: + notif = notif.instance() + self.assertIn('NotifyDbus', str(notif)) + + # call notify() + self.show_notification(notif) + + # check whether Mocks were called + dbus.SessionBus.assert_called_once() + + session_bus.get_object.assert_called_once_with( + 'org.freedesktop.Notifications', + '/org/freedesktop/Notifications' + ) + + interface.Notify.assert_called_once_with( + TestNotification.data['app_name'], + 0, + TestNotification.data['app_icon'], + TestNotification.data['title'], + TestNotification.data['message'], + [], {}, + TestNotification.data['timeout'] * 1000 + ) + finally: + del sys.modules['dbus'] + self.assertNotIn('dbus', sys.modules) + + @PlatformTest('linux') + def test_notification_notifysend(self): + ''' + Test mocked Linux notify-send for plyer.notification. + ''' + notif = platform_import( + platform='linux', + module_name='notification', + whereis_exe=MockedNotifySend.whereis_exe + ) + self.assertIn('NotifySendNotification', dir(notif)) + with patch(target='warnings.warn', new=MockedNotifySend.warn): + notif = notif.instance() + self.assertIn('NotifySendNotification', str(notif)) + + with patch(target='subprocess.call', new=MockedNotifySend.call): + self.assertIsNone(self.show_notification(notif)) + + +if __name__ == '__main__': + unittest.main() diff --git a/sbapp/plyer/tests/test_screenshot.py b/sbapp/plyer/tests/test_screenshot.py new file mode 100644 index 0000000..931fac2 --- /dev/null +++ b/sbapp/plyer/tests/test_screenshot.py @@ -0,0 +1,137 @@ +''' +TestScreenshot +============== + +Tested platforms: + +* MacOS +* Linux +''' + +import unittest + +from os import mkdir, remove +from os.path import join, expanduser, exists + +from mock import patch +from plyer.tests.common import PlatformTest, platform_import + + +class MockedScreenCapture: + ''' + Mocked object used instead of the console-like calling + of screencapture binary with parameters. + ''' + @staticmethod + def whereis_exe(binary): + ''' + Mock whereis_exe, so that it looks like + MacOS screencapture binary is present on the system. + ''' + return binary == 'screencapture' + + @staticmethod + def call(args): + ''' + Mocked subprocess.call to check console parameters. + ''' + assert len(args) == 2, len(args) + assert args[0] == 'screencapture', args + assert args[1] == join( + expanduser('~'), 'Pictures', 'screenshot.png' + ), args + with open(args[1], 'w') as scr: + scr.write('') + + +class MockedXWD: + ''' + Mocked object used instead of the console-like calling + of X11 xwd binary with parameters. + ''' + @staticmethod + def whereis_exe(binary): + ''' + Mock whereis_exe, so that it looks like + X11 xwd binary is present on the system. + ''' + return binary == 'xwd' + + @staticmethod + def call(args, stdout): + ''' + Mocked subprocess.call to check console parameters. + ''' + assert len(args) == 3, args + assert args[0] == 'xwd', args + assert args[1:] == ['-silent', '-root'], args + assert stdout.name == join( + expanduser('~'), 'Pictures', 'screenshot.xwd' + ), stdout.name + with open(stdout.name, 'w') as scr: + scr.write('') + + +class TestScreenshot(unittest.TestCase): + ''' + TestCase for plyer.screenshot. + ''' + + def setUp(self): + path = join(expanduser('~'), 'Pictures') + if not exists(path): + mkdir(path) + + @PlatformTest('macosx') + def test_screenshot_screencapture(self): + ''' + Test mocked MacOS screencapture for plyer.screenshot. + ''' + scr = platform_import( + platform='macosx', + module_name='screenshot', + whereis_exe=MockedScreenCapture.whereis_exe + ) + + # such class exists in screenshot module + self.assertIn('OSXScreenshot', dir(scr)) + + # the required instance is created + scr = scr.instance() + self.assertIn('OSXScreenshot', str(scr)) + + # move capture from context manager to run without mock + with patch(target='subprocess.call', new=MockedScreenCapture.call): + self.assertIsNone(scr.capture()) + + self.assertTrue(exists(scr.file_path)) + remove(scr.file_path) + + @PlatformTest('linux') + def test_screenshot_xwd(self): + ''' + Test mocked X11 xwd for plyer.screenshot. + ''' + scr = platform_import( + platform='linux', + module_name='screenshot', + whereis_exe=MockedXWD.whereis_exe + ) + + # such class exists in screenshot module + self.assertIn('LinuxScreenshot', dir(scr)) + + # the required instance is created + scr = scr.instance() + self.assertIn('LinuxScreenshot', str(scr)) + + # move capture from context manager to run without mock + with patch(target='subprocess.call', new=MockedXWD.call): + self.assertIsNone(scr.capture()) + + self.assertTrue(exists(scr.file_path)) + remove(scr.file_path) + + +if __name__ == '__main__': + unittest.main() diff --git a/sbapp/plyer/tests/test_storagepath.py b/sbapp/plyer/tests/test_storagepath.py new file mode 100644 index 0000000..35b8139 --- /dev/null +++ b/sbapp/plyer/tests/test_storagepath.py @@ -0,0 +1,72 @@ +''' +TestStoragePath +=============== + +Tested platforms: + +* macOS +''' + +import unittest + +from plyer.tests.common import platform_import, PlatformTest + + +class TestStoragePath(unittest.TestCase): + ''' + TestCase for plyer.storagepath. + ''' + + @PlatformTest('macosx') + def test_storagepath_macosx(self): + ''' + Test macOS for plyer.storagepath. + ''' + storagepath = platform_import( + platform='macosx', + module_name='storagepath' + ) + + self.assertIn('OSXStoragePath', dir(storagepath)) + storagepath = storagepath.instance() + self.assertIn('OSXStoragePath', str(storagepath)) + + path_format = 'file:///Users/' + + self.assertIn(path_format, storagepath.get_home_dir()) + self.assertIn('/', storagepath.get_root_dir()) + self.assertIn(path_format, storagepath.get_documents_dir()) + self.assertIn(path_format, storagepath.get_downloads_dir()) + self.assertIn(path_format, storagepath.get_videos_dir()) + self.assertIn(path_format, storagepath.get_music_dir()) + self.assertIn(path_format, storagepath.get_pictures_dir()) + self.assertIn(path_format, storagepath.get_application_dir()) + + @PlatformTest('win') + def test_storagepath_windows(self): + ''' + Test win for plyer.storagepath. + ''' + storagepath = platform_import( + platform='win', + module_name='storagepath' + ) + + self.assertIn('WinStoragePath', dir(storagepath)) + storagepath = storagepath.instance() + self.assertIn('WinStoragePath', str(storagepath)) + + path_format = ':\\' + + self.assertIn(path_format, storagepath.get_home_dir()) + self.assertIn(path_format, storagepath.get_root_dir()) + self.assertIn(path_format, storagepath.get_documents_dir()) + self.assertIn(path_format, storagepath.get_downloads_dir()) + self.assertIn(path_format, storagepath.get_videos_dir()) + self.assertIn(path_format, storagepath.get_music_dir()) + self.assertIn(path_format, storagepath.get_pictures_dir()) + self.assertIn(path_format, storagepath.get_application_dir()) + + +if __name__ == '__main__': + unittest.main() diff --git a/sbapp/plyer/tests/test_uniqueid.py b/sbapp/plyer/tests/test_uniqueid.py new file mode 100644 index 0000000..629c840 --- /dev/null +++ b/sbapp/plyer/tests/test_uniqueid.py @@ -0,0 +1,78 @@ +''' +TestUniqueID +============ + +Tested platforms: + +* Windows +''' + +import unittest +from mock import patch, Mock +from plyer.tests.common import PlatformTest, platform_import + + +class TestUniqueID(unittest.TestCase): + ''' + TestCase for plyer.uniqueid. + ''' + + def test_uniqueid(self): + ''' + General all platform test for plyer.uniqueid. + ''' + from plyer import uniqueid + self.assertTrue(len(uniqueid.id) > 0) + + @PlatformTest('win') + def test_uniqueid_win(self): + ''' + Test Windows API for plyer.uniqueid. + ''' + try: + from winreg import ( + HKEY_LOCAL_MACHINE as HKLM, + KEY_READ as READ, KEY_WOW64_64KEY as VIEW + ) + except ImportError: + from _winreg import ( + HKEY_LOCAL_MACHINE as HKLM, + KEY_READ as READ, KEY_WOW64_64KEY as VIEW + ) + + # mock the 'regedit' alias for winreg, + # see if the import passes and get the instance + regedit_mod = 'plyer.platforms.win.uniqueid.regedit' + with patch(target=regedit_mod): + uniqueid_ = platform_import( + platform='win', + module_name='uniqueid' + ) + uniqueid = uniqueid_.instance() + self.assertIsInstance(uniqueid_.regedit, Mock) + + # out of mocking block, regedit should be a winreg module + self.assertIsInstance(uniqueid_.regedit, type(unittest)) + + # OpenKey is supposed to return a handle to registry key + regedit_opkey = 'plyer.platforms.win.uniqueid.regedit.OpenKey' + with patch(target=regedit_opkey, return_value='unicorn') as opkey: + + # QueryValueEx is supposed to return 2 packed values + # (key, type_id) + queryval = 'plyer.platforms.win.uniqueid.regedit.QueryValueEx' + retval = ('unique', None) + with patch(target=queryval, return_value=retval) as query: + uid = uniqueid.id + opkey.assert_called_once_with( + # key, subkey + HKLM, r'SOFTWARE\\Microsoft\\Cryptography', + # reserved integer (has to be 0 - zero), access mask + 0, READ | VIEW + ) + query.assert_called_once_with('unicorn', 'MachineGuid') + self.assertEqual(uid, retval[0]) + + +if __name__ == '__main__': + unittest.main() diff --git a/sbapp/plyer/tests/test_utils.py b/sbapp/plyer/tests/test_utils.py new file mode 100644 index 0000000..3532b76 --- /dev/null +++ b/sbapp/plyer/tests/test_utils.py @@ -0,0 +1,417 @@ +''' +TestUtils +========= + +Tested platforms: + +* Android +* iOS +* Windows +* MacOS +* Linux +''' + +import unittest +from mock import patch + + +class TestUtils(unittest.TestCase): + ''' + TestCase for plyer.utils. + ''' + + def cutter(self, part, string): + ''' + Cut off a part of a string if it contains a substring, + otherwise raise an error. + ''' + self.assertIn(part, string) + return string[len(part):] + + def test_deprecated_function(self): + ''' + Test printed out warning with @deprecated decorator + on a function without any arguments. + ''' + + from plyer.utils import deprecated + + @deprecated + def function(): + ''' + Dummy deprecated function. + ''' + return 1 + + with patch(target='warnings.warn') as stderr: + self.assertEqual(function(), 1) + + args, _ = stderr.call_args_list[0] + args = args[0] + args = self.cutter('[WARNING] ', args) + args = self.cutter('deprecated function function', args) + args = self.cutter('test_utils.py', args) + args = self.cutter('Called from', args) + args = self.cutter('test_utils.py', args) + args = self.cutter('by test_deprecated_function().\n', args) + + args, _ = stderr.call_args_list[1] + self.assertEqual(args, ( + ''' + Dummy deprecated function. + ''', + )) + + def test_deprecated_function_arg(self): + ''' + Test printed out warning with @deprecated decorator + on a function with arguments. + ''' + + from plyer.utils import deprecated + + @deprecated + def function_with_arg(arg): + ''' + Dummy deprecated function with arg. + ''' + return arg + + with patch(target='warnings.warn') as stderr: + self.assertEqual(function_with_arg(1), 1) + + args, _ = stderr.call_args_list[0] + args = args[0] + args = self.cutter('[WARNING] ', args) + args = self.cutter('deprecated function function_with_arg', args) + args = self.cutter('test_utils.py', args) + args = self.cutter('Called from', args) + args = self.cutter('test_utils.py', args) + args = self.cutter('by test_deprecated_function_arg().\n', args) + + args, _ = stderr.call_args_list[1] + self.assertEqual(args, ( + ''' + Dummy deprecated function with arg. + ''', + )) + + def test_deprecated_function_kwarg(self): + ''' + Test printed out warning with @deprecated decorator + on a function with keyword arguments. + ''' + + from plyer.utils import deprecated + + @deprecated + def function_with_kwarg(kwarg): + ''' + Dummy deprecated function with kwarg. + ''' + return kwarg + + with patch(target='warnings.warn') as stderr: + self.assertEqual(function_with_kwarg(kwarg=1), 1) + + args, _ = stderr.call_args_list[0] + args = args[0] + args = self.cutter('[WARNING] ', args) + args = self.cutter('deprecated function function_with_kwarg', args) + args = self.cutter('test_utils.py', args) + args = self.cutter('Called from', args) + args = self.cutter('test_utils.py', args) + args = self.cutter('by test_deprecated_function_kwarg().\n', args) + + args, _ = stderr.call_args_list[1] + self.assertEqual(args, ( + ''' + Dummy deprecated function with kwarg. + ''', + )) + + def test_deprecated_class_method(self): + ''' + Test printed out warning with @deprecated decorator + on a instance bound method. + ''' + + from plyer.utils import deprecated + + class Class: + ''' + Dummy class with deprecated method method. + ''' + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + + @deprecated + def method(self): + ''' + Dummy deprecated method. + ''' + return (self.args, self.kwargs) + + with patch(target='warnings.warn') as stderr: + args = (1, 2, 3) + kwargs = dict(x=1, y=2) + + cls = Class(*args, **kwargs) + self.assertEqual(cls.method(), (args, kwargs)) + + args, kwargs = stderr.call_args_list[0] + args = args[0] + args = self.cutter('[WARNING] ', args) + args = self.cutter('deprecated function method', args) + args = self.cutter('test_utils.py', args) + args = self.cutter('Called from', args) + args = self.cutter('test_utils.py', args) + args = self.cutter('by test_deprecated_class_method().\n', args) + + args, kwargs = stderr.call_args_list[1] + self.assertEqual(args, ( + ''' + Dummy deprecated method. + ''', + )) + + def test_deprecated_class_static_none(self): + ''' + Test printed out warning with @deprecated decorator + on a static method without arguments. + ''' + + from plyer.utils import deprecated + + class Class: + ''' + Dummy class with deprecated static method. + ''' + args = None + kwargs = None + + def __init__(self, *args, **kwargs): + Class.args = args + Class.kwargs = kwargs + + @staticmethod + @deprecated + def static(): + ''' + Dummy deprecated static method. + ''' + return (Class.args, Class.kwargs) + + with patch(target='warnings.warn') as stderr: + self.assertEqual(Class.static(), (None, None)) + + args, _ = stderr.call_args_list[0] + args = args[0] + args = self.cutter('[WARNING] ', args) + args = self.cutter('deprecated function static', args) + args = self.cutter('test_utils.py', args) + args = self.cutter('Called from', args) + args = self.cutter('test_utils.py', args) + args = self.cutter( + 'by test_deprecated_class_static_none().\n', args + ) + + args, _ = stderr.call_args_list[1] + self.assertEqual(args, ( + ''' + Dummy deprecated static method. + ''', + )) + + def test_deprecated_class_static_argskwargs(self): + ''' + Test printed out warning with @deprecated decorator + on a static method with arguments and keyword argument. + ''' + + from plyer.utils import deprecated + + class Class: + ''' + Dummy class with deprecated static method. + ''' + args = None + kwargs = None + + def __init__(self, *args, **kwargs): + Class.args = args + Class.kwargs = kwargs + + @staticmethod + @deprecated + def static(): + ''' + Dummy deprecated static method. + ''' + return (Class.args, Class.kwargs) + + with patch(target='warnings.warn') as stderr: + args = (1, 2, 3) + kwargs = dict(x=1, y=2) + + cls = Class(*args, **kwargs) + self.assertEqual(cls.static(), (args, kwargs)) + + args, kwargs = stderr.call_args_list[0] + args = args[0] + args = self.cutter('[WARNING] ', args) + args = self.cutter('deprecated function static', args) + args = self.cutter('test_utils.py', args) + args = self.cutter('Called from', args) + args = self.cutter('test_utils.py', args) + args = self.cutter( + 'by test_deprecated_class_static_argskwargs().\n', args + ) + + args, kwargs = stderr.call_args_list[1] + self.assertEqual(args, ( + ''' + Dummy deprecated static method. + ''', + )) + + def test_deprecated_class_clsmethod(self): + ''' + Test printed out warning with @deprecated decorator + on a class bound method. + ''' + + from plyer.utils import deprecated + + class Class: + ''' + Dummy class with deprecated class method. + ''' + args = None + kwargs = None + + @classmethod + @deprecated + def clsmethod(cls): + ''' + Dummy deprecated class method. + ''' + return (cls.args, cls.kwargs) + + with patch(target='warnings.warn') as stderr: + self.assertEqual(Class.clsmethod(), (None, None)) + + args, _ = stderr.call_args_list[0] + args = args[0] + args = self.cutter('[WARNING] ', args) + args = self.cutter('deprecated function clsmethod', args) + args = self.cutter('test_utils.py', args) + args = self.cutter('Called from', args) + args = self.cutter('test_utils.py', args) + args = self.cutter('by test_deprecated_class_clsmethod().\n', args) + + args, _ = stderr.call_args_list[1] + self.assertEqual(args, ( + ''' + Dummy deprecated class method. + ''', + )) + + def test_deprecated_class(self): + ''' + Test printed out warning with @deprecated decorator on a class. + ''' + + from plyer.utils import deprecated + + @deprecated + class Class: + ''' + Dummy deprecated class. + ''' + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + + with patch(target='warnings.warn') as stderr: + args = (1, 2, 3) + kwargs = dict(x=1, y=2) + + cls = Class(*args, **kwargs) + self.assertIsInstance(cls, Class) + self.assertEqual(cls.args, args) + self.assertEqual(cls.kwargs, kwargs) + + args, _ = stderr.call_args_list[0] + args = args[0] + args = self.cutter('[WARNING] ', args) + args = self.cutter('Creating an instance', args) + args = self.cutter('deprecated class Class in', args) + args = self.cutter(__name__, args) + args = self.cutter('Called from', args) + args = self.cutter('test_utils.py', args) + args = self.cutter('by test_deprecated_class().\n', args) + + args, kwargs = stderr.call_args_list[1] + self.assertEqual(args, ( + ''' + Dummy deprecated class. + ''', + )) + + def test_deprecated_class_inherited(self): + ''' + Test printed out warning with @deprecated decorator on a class + which inherits from a deprecated class. + ''' + + from plyer.utils import deprecated + + @deprecated + class Class: + ''' + Dummy deprecated class. + ''' + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + + class Inherited(Class): + ''' + Dummy class inheriting from a dummy deprecated class. + ''' + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.args = args + self.kwargs = kwargs + + with patch(target='warnings.warn') as stderr: + args = (1, 2, 3) + kwargs = dict(x=1, y=2) + + cls = Inherited(*args, **kwargs) + self.assertIsInstance(cls, Inherited) + self.assertEqual(cls.args, args) + self.assertEqual(cls.kwargs, kwargs) + + args, _ = stderr.call_args_list[0] + args = args[0] + args = self.cutter('[WARNING] ', args) + args = self.cutter('Creating an instance', args) + args = self.cutter('deprecated class Class in', args) + args = self.cutter(__name__, args) + args = self.cutter('Called from', args) + args = self.cutter('test_utils.py', args) + args = self.cutter('by test_deprecated_class_inherited().\n', args) + + args, kwargs = stderr.call_args_list[1] + self.assertEqual(args, ( + ''' + Dummy deprecated class. + ''', + )) + + +if __name__ == '__main__': + unittest.main() diff --git a/sbapp/plyer/tools/pep8checker/pep8.py b/sbapp/plyer/tools/pep8checker/pep8.py new file mode 100644 index 0000000..a2f1f2c --- /dev/null +++ b/sbapp/plyer/tools/pep8checker/pep8.py @@ -0,0 +1,1953 @@ +#!/usr/bin/env python +# pep8.py - Check Python source code formatting, according to PEP 8 +# Copyright (C) 2006 Johann C. Rocholl +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# flake8: noqa +r""" +Check Python source code formatting, according to PEP 8: +http://www.python.org/dev/peps/pep-0008/ + +For usage and a list of options, try this: +$ python pep8.py -h + +This program and its regression test suite live here: +http://github.com/jcrocholl/pep8 + +Groups of errors and warnings: +E errors +W warnings +100 indentation +200 whitespace +300 blank lines +400 imports +500 line length +600 deprecation +700 statements +900 syntax error + +You can add checks to this program by writing plugins. Each plugin is +a simple function that is called for each line of source code, either +physical or logical. + +Physical line: +- Raw line of text from the input file. + +Logical line: +- Multi-line statements converted to a single line. +- Stripped left and right. +- Contents of strings replaced with 'xxx' of same length. +- Comments removed. + +The check function requests physical or logical lines by the name of +the first argument: + +def maximum_line_length(physical_line) +def extraneous_whitespace(logical_line) +def blank_lines(logical_line, blank_lines, indent_level, line_number) + +The last example above demonstrates how check plugins can request +additional information with extra arguments. All attributes of the +Checker object are available. Some examples: + +lines: a list of the raw lines from the input file +tokens: the tokens that contribute to this logical line +line_number: line number in the input file +blank_lines: blank lines before this one +indent_char: first indentation character in this file (' ' or '\t') +indent_level: indentation (with tabs expanded to multiples of 8) +previous_indent_level: indentation on previous line +previous_logical: previous logical line + +The docstring of each check function shall be the relevant part of +text from PEP 8. It is printed if the user enables --show-pep8. +Several docstrings contain examples directly from the PEP 8 document. + +Okay: spam(ham[1], {eggs: 2}) +E201: spam( ham[1], {eggs: 2}) + +These examples are verified automatically when pep8.py is run with the +--doctest option. You can add examples for your own check functions. +The format is simple: "Okay" or error/warning code followed by colon +and space, the rest of the line is example source code. If you put 'r' +before the docstring, you can use \n for newline, \t for tab and \s +for space. + +""" + +__version__ = '1.3.3' + +import os +import sys +import re +import time +import inspect +import keyword +import tokenize +from optparse import OptionParser +from fnmatch import fnmatch +try: + from ConfigParser import RawConfigParser +except ImportError: + from configparser import RawConfigParser + from io import TextIOWrapper + +DEFAULT_EXCLUDE = '.svn,CVS,.bzr,.hg,.git' +DEFAULT_IGNORE = 'E24' +if sys.platform == 'win32': + DEFAULT_CONFIG = os.path.expanduser(r'~\.pep8') +else: + DEFAULT_CONFIG = os.path.join(os.getenv('XDG_CONFIG_HOME') or + os.path.expanduser('~/.config'), 'pep8') +MAX_LINE_LENGTH = 80 +REPORT_FORMAT = { + 'default': '%(path)s:%(row)d:%(col)d: %(code)s %(text)s', + 'pylint': '%(path)s:%(row)d: [%(code)s] %(text)s', +} + + +SINGLETONS = frozenset(['False', 'None', 'True']) +KEYWORDS = frozenset(keyword.kwlist + ['print']) - SINGLETONS +BINARY_OPERATORS = frozenset([ + '**=', '*=', '+=', '-=', '!=', '<>', + '%=', '^=', '&=', '|=', '==', '/=', '//=', '<=', '>=', '<<=', '>>=', + '%', '^', '&', '|', '=', '/', '//', '<', '>', '<<']) +UNARY_OPERATORS = frozenset(['>>', '**', '*', '+', '-']) +OPERATORS = BINARY_OPERATORS | UNARY_OPERATORS +WHITESPACE = frozenset(' \t') +SKIP_TOKENS = frozenset([tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE, + tokenize.INDENT, tokenize.DEDENT]) +BENCHMARK_KEYS = ['directories', 'files', 'logical lines', 'physical lines'] + +INDENT_REGEX = re.compile(r'([ \t]*)') +RAISE_COMMA_REGEX = re.compile(r'raise\s+\w+\s*(,)') +RERAISE_COMMA_REGEX = re.compile(r'raise\s+\w+\s*,\s*\w+\s*,\s*\w+') +SELFTEST_REGEX = re.compile(r'(Okay|[EW]\d{3}):\s(.*)') +ERRORCODE_REGEX = re.compile(r'[EW]\d{3}') +DOCSTRING_REGEX = re.compile(r'u?r?["\']') +EXTRANEOUS_WHITESPACE_REGEX = re.compile(r'[[({] | []}),;:]') +WHITESPACE_AFTER_COMMA_REGEX = re.compile(r'[,;:]\s*(?: |\t)') +COMPARE_SINGLETON_REGEX = re.compile(r'([=!]=)\s*(None|False|True)') +COMPARE_TYPE_REGEX = re.compile(r'([=!]=|is|is\s+not)\s*type(?:s\.(\w+)Type' + r'|\(\s*(\(\s*\)|[^)]*[^ )])\s*\))') +KEYWORD_REGEX = re.compile(r'(?:[^\s])(\s*)\b(?:%s)\b(\s*)' % + r'|'.join(KEYWORDS)) +OPERATOR_REGEX = re.compile(r'(?:[^\s])(\s*)(?:[-+*/|!<=>%&^]+)(\s*)') +LAMBDA_REGEX = re.compile(r'\blambda\b') +HUNK_REGEX = re.compile(r'^@@ -\d+,\d+ \+(\d+),(\d+) @@.*$') + +# Work around Python < 2.6 behaviour, which does not generate NL after +# a comment which is on a line by itself. +COMMENT_WITH_NL = tokenize.generate_tokens(['#\n'].pop).send(None)[1] == '#\n' + + +############################################################################## +# Plugins (check functions) for physical lines +############################################################################## + + +def tabs_or_spaces(physical_line, indent_char): + r""" + Never mix tabs and spaces. + + The most popular way of indenting Python is with spaces only. The + second-most popular way is with tabs only. Code indented with a mixture + of tabs and spaces should be converted to using spaces exclusively. When + invoking the Python command line interpreter with the -t option, it issues + warnings about code that illegally mixes tabs and spaces. When using -tt + these warnings become errors. These options are highly recommended! + + Okay: if a == 0:\n a = 1\n b = 1 + E101: if a == 0:\n a = 1\n\tb = 1 + """ + indent = INDENT_REGEX.match(physical_line).group(1) + for offset, char in enumerate(indent): + if char != indent_char: + return offset, "E101 indentation contains mixed spaces and tabs" + + +def tabs_obsolete(physical_line): + r""" + For new projects, spaces-only are strongly recommended over tabs. Most + editors have features that make this easy to do. + + Okay: if True:\n return + W191: if True:\n\treturn + """ + indent = INDENT_REGEX.match(physical_line).group(1) + if '\t' in indent: + return indent.index('\t'), "W191 indentation contains tabs" + + +def trailing_whitespace(physical_line): + r""" + JCR: Trailing whitespace is superfluous. + FBM: Except when it occurs as part of a blank line (i.e. the line is + nothing but whitespace). According to Python docs[1] a line with only + whitespace is considered a blank line, and is to be ignored. However, + matching a blank line to its indentation level avoids mistakenly + terminating a multi-line statement (e.g. class declaration) when + pasting code into the standard Python interpreter. + + [1] http://docs.python.org/reference/lexical_analysis.html#blank-lines + + The warning returned varies on whether the line itself is blank, for easier + filtering for those who want to indent their blank lines. + + Okay: spam(1) + W291: spam(1)\s + W293: class Foo:\n \n bang = 12 + """ + physical_line = physical_line.rstrip('\n') # chr(10), newline + physical_line = physical_line.rstrip('\r') # chr(13), carriage return + physical_line = physical_line.rstrip('\x0c') # chr(12), form feed, ^L + stripped = physical_line.rstrip(' \t\v') + if physical_line != stripped: + if stripped: + return len(stripped), "W291 trailing whitespace" + else: + return 0, "W293 blank line contains whitespace" + + +# def trailing_blank_lines(physical_line, lines, line_number): +# r""" +# JCR: Trailing blank lines are superfluous. +# +# Okay: spam(1) +# W391: spam(1)\n +# """ +# if not physical_line.rstrip() and line_number == len(lines): +# return 0, "W391 blank line at end of file" + + +def missing_newline(physical_line): + """ + JCR: The last line should have a newline. + + Reports warning W292. + """ + if physical_line.rstrip() == physical_line: + return len(physical_line), "W292 no newline at end of file" + + +def maximum_line_length(physical_line, max_line_length): + """ + Limit all lines to a maximum of 79 characters. + + There are still many devices around that are limited to 80 character + lines; plus, limiting windows to 80 characters makes it possible to have + several windows side-by-side. The default wrapping on such devices looks + ugly. Therefore, please limit all lines to a maximum of 79 characters. + For flowing long blocks of text (docstrings or comments), limiting the + length to 72 characters is recommended. + + Reports error E501. + """ + line = physical_line.rstrip() + length = len(line) + if length > max_line_length: + if hasattr(line, 'decode'): # Python 2 + # The line could contain multi-byte characters + try: + length = len(line.decode('utf-8')) + except UnicodeError: + pass + if length > max_line_length: + return (max_line_length, "E501 line too long " + "(%d > %d characters)" % (length, max_line_length)) + + +############################################################################## +# Plugins (check functions) for logical lines +############################################################################## + + +def blank_lines(logical_line, blank_lines, indent_level, line_number, + previous_logical, previous_indent_level): + r""" + Separate top-level function and class definitions with two blank lines. + + Method definitions inside a class are separated by a single blank line. + + Extra blank lines may be used (sparingly) to separate groups of related + functions. Blank lines may be omitted between a bunch of related + one-liners (e.g. a set of dummy implementations). + + Use blank lines in functions, sparingly, to indicate logical sections. + + Okay: def a():\n pass\n\n\ndef b():\n pass + Okay: def a():\n pass\n\n\n# Foo\n# Bar\n\ndef b():\n pass + + E301: class Foo:\n b = 0\n def bar():\n pass + E302: def a():\n pass\n\ndef b(n):\n pass + E303: def a():\n pass\n\n\n\ndef b(n):\n pass + E303: def a():\n\n\n\n pass + E304: @decorator\n\ndef a():\n pass + """ + if line_number == 1: + return # Don't expect blank lines before the first line + if previous_logical.startswith('@'): + if blank_lines: + yield 0, "E304 blank lines found after function decorator" + elif blank_lines > 2 or (indent_level and blank_lines == 2): + yield 0, "E303 too many blank lines (%d)" % blank_lines + elif logical_line.startswith(('def ', 'class ', '@')): + if indent_level: + if not (blank_lines or previous_indent_level < indent_level or + DOCSTRING_REGEX.match(previous_logical)): + yield 0, "E301 expected 1 blank line, found 0" + elif blank_lines != 2: + yield 0, "E302 expected 2 blank lines, found %d" % blank_lines + + +def extraneous_whitespace(logical_line): + """ + Avoid extraneous whitespace in the following situations: + + - Immediately inside parentheses, brackets or braces. + + - Immediately before a comma, semicolon, or colon. + + Okay: spam(ham[1], {eggs: 2}) + E201: spam( ham[1], {eggs: 2}) + E201: spam(ham[ 1], {eggs: 2}) + E201: spam(ham[1], { eggs: 2}) + E202: spam(ham[1], {eggs: 2} ) + E202: spam(ham[1 ], {eggs: 2}) + E202: spam(ham[1], {eggs: 2 }) + + E203: if x == 4: print x, y; x, y = y , x + E203: if x == 4: print x, y ; x, y = y, x + E203: if x == 4 : print x, y; x, y = y, x + """ + line = logical_line + for match in EXTRANEOUS_WHITESPACE_REGEX.finditer(line): + text = match.group() + char = text.strip() + found = match.start() + if text == char + ' ': + # assert char in '([{' + yield found + 1, "E201 whitespace after '%s'" % char + elif line[found - 1] != ',': + code = ('E202' if char in '}])' else 'E203') # if char in ',;:' + yield found, "%s whitespace before '%s'" % (code, char) + + +def whitespace_around_keywords(logical_line): + r""" + Avoid extraneous whitespace around keywords. + + Okay: True and False + E271: True and False + E272: True and False + E273: True and\tFalse + E274: True\tand False + """ + for match in KEYWORD_REGEX.finditer(logical_line): + before, after = match.groups() + + if '\t' in before: + yield match.start(1), "E274 tab before keyword" + elif len(before) > 1: + yield match.start(1), "E272 multiple spaces before keyword" + + if '\t' in after: + yield match.start(2), "E273 tab after keyword" + elif len(after) > 1: + yield match.start(2), "E271 multiple spaces after keyword" + + +def missing_whitespace(logical_line): + """ + JCR: Each comma, semicolon or colon should be followed by whitespace. + + Okay: [a, b] + Okay: (3,) + Okay: a[1:4] + Okay: a[:4] + Okay: a[1:] + Okay: a[1:4:2] + E231: ['a','b'] + E231: foo(bar,baz) + """ + line = logical_line + for index in range(len(line) - 1): + char = line[index] + if char in ',;:' and line[index + 1] not in WHITESPACE: + before = line[:index] + if char == ':' and before.count('[') > before.count(']'): + continue # Slice syntax, no space required + if char == ',' and line[index + 1] == ')': + continue # Allow tuple with only one element: (3,) + yield index, "E231 missing whitespace after '%s'" % char + + +def indentation(logical_line, previous_logical, indent_char, + indent_level, previous_indent_level): + r""" + Use 4 spaces per indentation level. + + For really old code that you don't want to mess up, you can continue to + use 8-space tabs. + + Okay: a = 1 + Okay: if a == 0:\n a = 1 + E111: .....a = 1 + + Okay: for item in items:\n pass + E112: for item in items:\npass + + Okay: a = 1\nb = 2 + E113: a = 1\n b = 2 + """ + if indent_char == ' ' and indent_level % 4: + yield 0, "E111 indentation is not a multiple of four" + indent_expect = previous_logical.endswith(':') + if indent_expect and indent_level <= previous_indent_level: + yield 0, "E112 expected an indented block" + if indent_level > previous_indent_level and not indent_expect: + yield 0, "E113 unexpected indentation" + + +def continuation_line_indentation(logical_line, tokens, indent_level, verbose): + r""" + Continuation lines should align wrapped elements either vertically using + Python's implicit line joining inside parentheses, brackets and braces, or + using a hanging indent. + + When using a hanging indent the following considerations should be applied: + + - there should be no arguments on the first line, and + + - further indentation should be used to clearly distinguish itself as a + continuation line. + + Okay: a = (\n) + E123: a = (\n ) + + Okay: a = (\n 42) + E121: a = (\n 42) + E122: a = (\n42) + E123: a = (\n 42\n ) + E124: a = (24,\n 42\n) + E125: if (a or\n b):\n pass + E126: a = (\n 42) + E127: a = (24,\n 42) + E128: a = (24,\n 42) + """ + first_row = tokens[0][2][0] + nrows = 1 + tokens[-1][2][0] - first_row + if nrows == 1: + return + + # indent_next tells us whether the next block is indented; assuming + # that it is indented by 4 spaces, then we should not allow 4-space + # indents on the final continuation line; in turn, some other + # indents are allowed to have an extra 4 spaces. + indent_next = logical_line.endswith(':') + + row = depth = 0 + # remember how many brackets were opened on each line + parens = [0] * nrows + # relative indents of physical lines + rel_indent = [0] * nrows + # visual indents + indent = [indent_level] + indent_chances = {} + last_indent = (0, 0) + if verbose >= 3: + print((">>> " + tokens[0][4].rstrip())) + + for token_type, text, start, end, line in tokens: + newline = row < start[0] - first_row + if newline: + row = start[0] - first_row + newline = (not last_token_multiline and + token_type not in (tokenize.NL, tokenize.NEWLINE)) + + if newline: + # this is the beginning of a continuation line. + last_indent = start + if verbose >= 3: + print(("... " + line.rstrip())) + + # record the initial indent. + rel_indent[row] = start[1] - indent_level + + if depth: + # a bracket expression in a continuation line. + # find the line that it was opened on + for open_row in range(row - 1, -1, -1): + if parens[open_row]: + break + else: + # an unbracketed continuation line (ie, backslash) + open_row = 0 + hang = rel_indent[row] - rel_indent[open_row] + visual_indent = indent_chances.get(start[1]) + + if token_type == tokenize.OP and text in ']})': + # this line starts with a closing bracket + if indent[depth]: + if start[1] != indent[depth]: + yield (start, 'E124 closing bracket does not match ' + 'visual indentation') + elif hang: + yield (start, 'E123 closing bracket does not match ' + 'indentation of opening bracket\'s line') + elif visual_indent is True: + # visual indent is verified + if not indent[depth]: + indent[depth] = start[1] + elif visual_indent in (text, str): + # ignore token lined up with matching one from a previous line + pass + elif indent[depth] and start[1] < indent[depth]: + # visual indent is broken + yield (start, 'E128 continuation line ' + 'under-indented for visual indent') + elif hang == 4 or (indent_next and rel_indent[row] == 8): + # hanging indent is verified + pass + else: + # indent is broken + if hang <= 0: + error = 'E122', 'missing indentation or outdented' + elif indent[depth]: + error = 'E127', 'over-indented for visual indent' + elif hang % 4: + error = 'E121', 'indentation is not a multiple of four' + else: + error = 'E126', 'over-indented for hanging indent' + yield start, "%s continuation line %s" % error + + # look for visual indenting + if parens[row] and token_type != tokenize.NL and not indent[depth]: + indent[depth] = start[1] + indent_chances[start[1]] = True + if verbose >= 4: + print(("bracket depth %s indent to %s" % (depth, start[1]))) + # deal with implicit string concatenation + elif token_type == tokenize.STRING or text in ('u', 'ur', 'b', 'br'): + indent_chances[start[1]] = str + + # keep track of bracket depth + if token_type == tokenize.OP: + if text in '([{': + depth += 1 + indent.append(0) + parens[row] += 1 + if verbose >= 4: + print(("bracket depth %s seen, col %s, visual min = %s" % + (depth, start[1], indent[depth]))) + elif text in ')]}' and depth > 0: + # parent indents should not be more than this one + prev_indent = indent.pop() or last_indent[1] + for d in range(depth): + if indent[d] > prev_indent: + indent[d] = 0 + for ind in list(indent_chances): + if ind >= prev_indent: + del indent_chances[ind] + depth -= 1 + if depth: + indent_chances[indent[depth]] = True + for idx in range(row, -1, -1): + if parens[idx]: + parens[idx] -= 1 + break + assert len(indent) == depth + 1 + if start[1] not in indent_chances: + # allow to line up tokens + indent_chances[start[1]] = text + + last_token_multiline = (start[0] != end[0]) + + if indent_next and rel_indent[-1] == 4: + yield (last_indent, "E125 continuation line does not distinguish " + "itself from next logical line") + + +def whitespace_before_parameters(logical_line, tokens): + """ + Avoid extraneous whitespace in the following situations: + + - Immediately before the open parenthesis that starts the argument + list of a function call. + + - Immediately before the open parenthesis that starts an indexing or + slicing. + + Okay: spam(1) + E211: spam (1) + + Okay: dict['key'] = list[index] + E211: dict ['key'] = list[index] + E211: dict['key'] = list [index] + """ + prev_type = tokens[0][0] + prev_text = tokens[0][1] + prev_end = tokens[0][3] + for index in range(1, len(tokens)): + token_type, text, start, end, line = tokens[index] + if (token_type == tokenize.OP and + text in '([' and + start != prev_end and + (prev_type == tokenize.NAME or prev_text in '}])') and + # Syntax "class A (B):" is allowed, but avoid it + (index < 2 or tokens[index - 2][1] != 'class') and + # Allow "return (a.foo for a in range(5))" + not keyword.iskeyword(prev_text)): + yield prev_end, "E211 whitespace before '%s'" % text + prev_type = token_type + prev_text = text + prev_end = end + + +def whitespace_around_operator(logical_line): + r""" + Avoid extraneous whitespace in the following situations: + + - More than one space around an assignment (or other) operator to + align it with another. + + Okay: a = 12 + 3 + E221: a = 4 + 5 + E222: a = 4 + 5 + E223: a = 4\t+ 5 + E224: a = 4 +\t5 + """ + for match in OPERATOR_REGEX.finditer(logical_line): + before, after = match.groups() + + if '\t' in before: + yield match.start(1), "E223 tab before operator" + elif len(before) > 1: + yield match.start(1), "E221 multiple spaces before operator" + + if '\t' in after: + yield match.start(2), "E224 tab after operator" + elif len(after) > 1: + yield match.start(2), "E222 multiple spaces after operator" + + +def missing_whitespace_around_operator(logical_line, tokens): + r""" + - Always surround these binary operators with a single space on + either side: assignment (=), augmented assignment (+=, -= etc.), + comparisons (==, <, >, !=, <>, <=, >=, in, not in, is, is not), + Booleans (and, or, not). + + - Use spaces around arithmetic operators. + + Okay: i = i + 1 + Okay: submitted += 1 + Okay: x = x * 2 - 1 + Okay: hypot2 = x * x + y * y + Okay: c = (a + b) * (a - b) + Okay: foo(bar, key='word', *args, **kwargs) + Okay: baz(**kwargs) + Okay: negative = -1 + Okay: spam(-1) + Okay: alpha[:-i] + Okay: if not -5 < x < +5:\n pass + Okay: lambda *args, **kw: (args, kw) + + E225: i=i+1 + E225: submitted +=1 + E225: x = x*2 - 1 + E225: hypot2 = x*x + y*y + E225: c = (a+b) * (a-b) + E225: c = alpha -4 + E225: z = x **y + """ + parens = 0 + need_space = False + prev_type = tokenize.OP + prev_text = prev_end = None + for token_type, text, start, end, line in tokens: + if token_type in (tokenize.NL, tokenize.NEWLINE, tokenize.ERRORTOKEN): + # ERRORTOKEN is triggered by backticks in Python 3000 + continue + if text in ('(', 'lambda'): + parens += 1 + elif text == ')': + parens -= 1 + if need_space: + if start != prev_end: + need_space = False + elif text == '>' and prev_text in ('<', '-'): + # Tolerate the "<>" operator, even if running Python 3 + # Deal with Python 3's annotated return value "->" + pass + else: + yield prev_end, "E225 missing whitespace around operator" + need_space = False + elif token_type == tokenize.OP and prev_end is not None: + if text == '=' and parens: + # Allow keyword args or defaults: foo(bar=None). + pass + elif text in BINARY_OPERATORS: + need_space = True + elif text in UNARY_OPERATORS: + # Allow unary operators: -123, -x, +1. + # Allow argument unpacking: foo(*args, **kwargs). + if prev_type == tokenize.OP: + if prev_text in '}])': + need_space = True + elif prev_type == tokenize.NAME: + if prev_text not in KEYWORDS: + need_space = True + elif prev_type not in SKIP_TOKENS: + need_space = True + if need_space and start == prev_end: + yield prev_end, "E225 missing whitespace around operator" + need_space = False + prev_type = token_type + prev_text = text + prev_end = end + + +def whitespace_around_comma(logical_line): + r""" + Avoid extraneous whitespace in the following situations: + + - More than one space around an assignment (or other) operator to + align it with another. + + Note: these checks are disabled by default + + Okay: a = (1, 2) + E241: a = (1, 2) + E242: a = (1,\t2) + """ + line = logical_line + for m in WHITESPACE_AFTER_COMMA_REGEX.finditer(line): + found = m.start() + 1 + if '\t' in m.group(): + yield found, "E242 tab after '%s'" % m.group()[0] + else: + yield found, "E241 multiple spaces after '%s'" % m.group()[0] + + +def whitespace_around_named_parameter_equals(logical_line, tokens): + """ + Don't use spaces around the '=' sign when used to indicate a + keyword argument or a default parameter value. + + Okay: def complex(real, imag=0.0): + Okay: return magic(r=real, i=imag) + Okay: boolean(a == b) + Okay: boolean(a != b) + Okay: boolean(a <= b) + Okay: boolean(a >= b) + + E251: def complex(real, imag = 0.0): + E251: return magic(r = real, i = imag) + """ + parens = 0 + no_space = False + prev_end = None + for token_type, text, start, end, line in tokens: + if no_space: + no_space = False + if start != prev_end: + yield (prev_end, + "E251 no spaces around keyword / parameter equals") + elif token_type == tokenize.OP: + if text == '(': + parens += 1 + elif text == ')': + parens -= 1 + elif parens and text == '=': + no_space = True + if start != prev_end: + yield (prev_end, + "E251 no spaces around keyword / parameter equals") + prev_end = end + + +def whitespace_before_inline_comment(logical_line, tokens): + """ + Separate inline comments by at least two spaces. + + An inline comment is a comment on the same line as a statement. Inline + comments should be separated by at least two spaces from the statement. + They should start with a # and a single space. + + Okay: x = x + 1 # Increment x + Okay: x = x + 1 # Increment x + E261: x = x + 1 # Increment x + E262: x = x + 1 #Increment x + E262: x = x + 1 # Increment x + """ + prev_end = (0, 0) + for token_type, text, start, end, line in tokens: + if token_type == tokenize.COMMENT: + if not line[:start[1]].strip(): + continue + if prev_end[0] == start[0] and start[1] < prev_end[1] + 2: + yield (prev_end, + "E261 at least two spaces before inline comment") + if text.startswith('# ') or not text.startswith('# '): + yield start, "E262 inline comment should start with '# '" + elif token_type != tokenize.NL: + prev_end = end + + +def imports_on_separate_lines(logical_line): + r""" + Imports should usually be on separate lines. + + Okay: import os\nimport sys + E401: import sys, os + + Okay: from subprocess import Popen, PIPE + Okay: from myclas import MyClass + Okay: from foo.bar.yourclass import YourClass + Okay: import myclass + Okay: import foo.bar.yourclass + """ + line = logical_line + if line.startswith('import '): + found = line.find(',') + if -1 < found: + yield found, "E401 multiple imports on one line" + + +def compound_statements(logical_line): + r""" + Compound statements (multiple statements on the same line) are + generally discouraged. + + While sometimes it's okay to put an if/for/while with a small body + on the same line, never do this for multi-clause statements. Also + avoid folding such long lines! + + Okay: if foo == 'blah':\n do_blah_thing() + Okay: do_one() + Okay: do_two() + Okay: do_three() + + E701: if foo == 'blah': do_blah_thing() + E701: for x in lst: total += x + E701: while t < 10: t = delay() + E701: if foo == 'blah': do_blah_thing() + E701: else: do_non_blah_thing() + E701: try: something() + E701: finally: cleanup() + E701: if foo == 'blah': one(); two(); three() + + E702: do_one(); do_two(); do_three() + """ + line = logical_line + found = line.find(':') + if -1 < found < len(line) - 1: + before = line[:found] + if (before.count('{') <= before.count('}') and # {'a': 1} (dict) + before.count('[') <= before.count(']') and # [1:2] (slice) + before.count('(') <= before.count(')') and # (Python 3 annotation) + not LAMBDA_REGEX.search(before)): # lambda x: x + yield found, "E701 multiple statements on one line (colon)" + found = line.find(';') + if -1 < found: + yield found, "E702 multiple statements on one line (semicolon)" + + +def explicit_line_join(logical_line, tokens): + r""" + Avoid explicit line join between brackets. + + The preferred way of wrapping long lines is by using Python's implied line + continuation inside parentheses, brackets and braces. Long lines can be + broken over multiple lines by wrapping expressions in parentheses. These + should be used in preference to using a backslash for line continuation. + + E502: aaa = [123, \\n 123] + E502: aaa = ("bbb " \\n "ccc") + + Okay: aaa = [123,\n 123] + Okay: aaa = ("bbb "\n "ccc") + Okay: aaa = "bbb " \\n "ccc" + """ + prev_start = prev_end = parens = 0 + for token_type, text, start, end, line in tokens: + if start[0] != prev_start and parens and backslash: + yield backslash, "E502 the backslash is redundant between brackets" + if end[0] != prev_end: + if line.rstrip('\r\n').endswith('\\'): + backslash = (end[0], len(line.splitlines()[-1]) - 1) + else: + backslash = None + prev_start = prev_end = end[0] + else: + prev_start = start[0] + if token_type == tokenize.OP: + if text in '([{': + parens += 1 + elif text in ')]}': + parens -= 1 + + +def comparison_to_singleton(logical_line): + """ + Comparisons to singletons like None should always be done + with "is" or "is not", never the equality operators. + + Okay: if arg is not None: + E711: if arg != None: + E712: if arg == True: + + Also, beware of writing if x when you really mean if x is not None -- + e.g. when testing whether a variable or argument that defaults to None was + set to some other value. The other value might have a type (such as a + container) that could be false in a boolean context! + """ + match = COMPARE_SINGLETON_REGEX.search(logical_line) + if match: + same = (match.group(1) == '==') + singleton = match.group(2) + msg = "'if cond is %s:'" % (('' if same else 'not ') + singleton) + if singleton in ('None',): + code = 'E711' + else: + code = 'E712' + nonzero = ((singleton == 'True' and same) or + (singleton == 'False' and not same)) + msg += " or 'if %scond:'" % ('' if nonzero else 'not ') + yield match.start(1), ("%s comparison to %s should be %s" % + (code, singleton, msg)) + + +def comparison_type(logical_line): + """ + Object type comparisons should always use isinstance() instead of + comparing types directly. + + Okay: if isinstance(obj, int): + E721: if type(obj) is type(1): + + When checking if an object is a string, keep in mind that it might be a + unicode string too! In Python 2.3, str and unicode have a common base + class, basestring, so you can do: + + Okay: if isinstance(obj, basestring): + Okay: if type(a1) is type(b1): + """ + match = COMPARE_TYPE_REGEX.search(logical_line) + if match: + inst = match.group(3) + if inst and isidentifier(inst) and inst not in SINGLETONS: + return # Allow comparison for types which are not obvious + yield match.start(1), "E721 do not compare types, use 'isinstance()'" + + +def python_3000_has_key(logical_line): + r""" + The {}.has_key() method will be removed in the future version of + Python. Use the 'in' operation instead. + + Okay: if "alph" in d:\n print d["alph"] + W601: assert d.has_key('alph') + """ + pos = logical_line.find('.has_key(') + if pos > -1: + yield pos, "W601 .has_key() is deprecated, use 'in'" + + +def python_3000_raise_comma(logical_line): + """ + When raising an exception, use "raise ValueError('message')" + instead of the older form "raise ValueError, 'message'". + + The paren-using form is preferred because when the exception arguments + are long or include string formatting, you don't need to use line + continuation characters thanks to the containing parentheses. The older + form will be removed in Python 3000. + + Okay: raise DummyError("Message") + W602: raise DummyError, "Message" + """ + match = RAISE_COMMA_REGEX.match(logical_line) + if match and not RERAISE_COMMA_REGEX.match(logical_line): + yield match.start(1), "W602 deprecated form of raising exception" + + +def python_3000_not_equal(logical_line): + """ + != can also be written <>, but this is an obsolete usage kept for + backwards compatibility only. New code should always use !=. + The older syntax is removed in Python 3000. + + Okay: if a != 'no': + W603: if a <> 'no': + """ + pos = logical_line.find('<>') + if pos > -1: + yield pos, "W603 '<>' is deprecated, use '!='" + + +def python_3000_backticks(logical_line): + """ + Backticks are removed in Python 3000. + Use repr() instead. + + Okay: val = repr(1 + 2) + W604: val = `1 + 2` + """ + pos = logical_line.find('`') + if pos > -1: + yield pos, "W604 backticks are deprecated, use 'repr()'" + + +############################################################################## +# Helper functions +############################################################################## + + +if '' == ''.encode(): + # Python 2: implicit encoding. + def readlines(filename): + f = open(filename) + try: + return f.readlines() + finally: + f.close() + + isidentifier = re.compile(r'[a-zA-Z_]\w*').match + stdin_get_value = sys.stdin.read +else: + # Python 3 + def readlines(filename): + f = open(filename, 'rb') + try: + coding, lines = tokenize.detect_encoding(f.readline) + f = TextIOWrapper(f, coding, line_buffering=True) + return [l.decode(coding) for l in lines] + f.readlines() + except (LookupError, SyntaxError, UnicodeError): + f.close() + # Fall back if files are improperly declared + f = open(filename, encoding='latin-1') + return f.readlines() + finally: + f.close() + + isidentifier = str.isidentifier + stdin_get_value = TextIOWrapper(sys.stdin.buffer, errors='ignore').read +readlines.__doc__ = " Read the source code." + + +def expand_indent(line): + r""" + Return the amount of indentation. + Tabs are expanded to the next multiple of 8. + + >>> expand_indent(' ') + 4 + >>> expand_indent('\t') + 8 + >>> expand_indent(' \t') + 8 + >>> expand_indent(' \t') + 8 + >>> expand_indent(' \t') + 16 + """ + if '\t' not in line: + return len(line) - len(line.lstrip()) + result = 0 + for char in line: + if char == '\t': + result = result // 8 * 8 + 8 + elif char == ' ': + result += 1 + else: + break + return result + + +def mute_string(text): + """ + Replace contents with 'xxx' to prevent syntax matching. + + >>> mute_string('"abc"') + '"xxx"' + >>> mute_string("'''abc'''") + "'''xxx'''" + >>> mute_string("r'abc'") + "r'xxx'" + """ + # String modifiers (e.g. u or r) + start = text.index(text[-1]) + 1 + end = len(text) - 1 + # Triple quotes + if text[-3:] in ('"""', "'''"): + start += 2 + end -= 2 + return text[:start] + 'x' * (end - start) + text[end:] + + +def parse_udiff(diff, patterns=None, parent='.'): + rv = {} + path = nrows = None + for line in diff.splitlines(): + if nrows: + if line[:1] != '-': + nrows -= 1 + continue + if line[:3] == '@@ ': + row, nrows = [int(g) for g in HUNK_REGEX.match(line).groups()] + rv[path].update(list(range(row, row + nrows))) + elif line[:3] == '+++': + path = line[4:].split('\t', 1)[0] + if path[:2] == 'b/': + path = path[2:] + rv[path] = set() + return dict([(os.path.join(parent, path), rows) + for (path, rows) in list(rv.items()) + if rows and filename_match(path, patterns)]) + + +def filename_match(filename, patterns, default=True): + """ + Check if patterns contains a pattern that matches filename. + If patterns is unspecified, this always returns True. + """ + if not patterns: + return default + return any(fnmatch(filename, pattern) for pattern in patterns) + + +############################################################################## +# Framework to run all checks +############################################################################## + + +def find_checks(argument_name): + """ + Find all globally visible functions where the first argument name + starts with argument_name. + """ + for name, function in list(globals().items()): + if not inspect.isfunction(function): + continue + args = inspect.getargspec(function)[0] + if args and args[0].startswith(argument_name): + codes = ERRORCODE_REGEX.findall(function.__doc__ or '') + yield name, codes, function, args + + +class Checker: + """ + Load a Python source file, tokenize it, check coding style. + """ + + def __init__(self, filename, lines=None, + options=None, report=None, **kwargs): + if options is None: + options = StyleGuide(kwargs).options + else: + assert not kwargs + self._io_error = None + self._physical_checks = options.physical_checks + self._logical_checks = options.logical_checks + self.max_line_length = options.max_line_length + self.verbose = options.verbose + self.filename = filename + if filename is None: + self.filename = 'stdin' + self.lines = lines or [] + elif lines is None: + try: + self.lines = readlines(filename) + except IOError: + exc_type, exc = sys.exc_info()[:2] + self._io_error = '%s: %s' % (exc_type.__name__, exc) + self.lines = [] + else: + self.lines = lines + self.report = report or options.report + self.report_error = self.report.error + + def readline(self): + """ + Get the next line from the input buffer. + """ + self.line_number += 1 + if self.line_number > len(self.lines): + return '' + return self.lines[self.line_number - 1] + + def readline_check_physical(self): + """ + Check and return the next physical line. This method can be + used to feed tokenize.generate_tokens. + """ + line = self.readline() + if line: + self.check_physical(line) + return line + + def run_check(self, check, argument_names): + """ + Run a check plugin. + """ + arguments = [] + for name in argument_names: + arguments.append(getattr(self, name)) + return check(*arguments) + + def check_physical(self, line): + """ + Run all physical checks on a raw input line. + """ + self.physical_line = line + if self.indent_char is None and line[:1] in WHITESPACE: + self.indent_char = line[0] + for name, check, argument_names in self._physical_checks: + result = self.run_check(check, argument_names) + if result is not None: + offset, text = result + self.report_error(self.line_number, offset, text, check) + + def build_tokens_line(self): + """ + Build a logical line from tokens. + """ + self.mapping = [] + logical = [] + length = 0 + previous = None + for token in self.tokens: + token_type, text = token[0:2] + if token_type in SKIP_TOKENS: + continue + if token_type == tokenize.STRING: + text = mute_string(text) + if previous: + end_row, end = previous[3] + start_row, start = token[2] + if end_row != start_row: # different row + prev_text = self.lines[end_row - 1][end - 1] + if prev_text == ',' or (prev_text not in '{[(' + and text not in '}])'): + logical.append(' ') + length += 1 + elif end != start: # different column + fill = self.lines[end_row - 1][end:start] + logical.append(fill) + length += len(fill) + self.mapping.append((length, token)) + logical.append(text) + length += len(text) + previous = token + self.logical_line = ''.join(logical) + assert self.logical_line.strip() == self.logical_line + + def check_logical(self): + """ + Build a line from tokens and run all logical checks on it. + """ + self.build_tokens_line() + self.report.increment_logical_line() + first_line = self.lines[self.mapping[0][1][2][0] - 1] + indent = first_line[:self.mapping[0][1][2][1]] + self.previous_indent_level = self.indent_level + self.indent_level = expand_indent(indent) + if self.verbose >= 2: + print((self.logical_line[:80].rstrip())) + for name, check, argument_names in self._logical_checks: + if self.verbose >= 4: + print((' ' + name)) + for result in self.run_check(check, argument_names): + offset, text = result + if isinstance(offset, tuple): + orig_number, orig_offset = offset + else: + for token_offset, token in self.mapping: + if offset >= token_offset: + orig_number = token[2][0] + orig_offset = (token[2][1] + offset - token_offset) + self.report_error(orig_number, orig_offset, text, check) + self.previous_logical = self.logical_line + + def generate_tokens(self): + if self._io_error: + self.report_error(1, 0, 'E902 %s' % self._io_error, readlines) + tokengen = tokenize.generate_tokens(self.readline_check_physical) + try: + for token in tokengen: + yield token + except (SyntaxError, tokenize.TokenError): + exc_type, exc = sys.exc_info()[:2] + offset = exc.args[1] + if len(offset) > 2: + offset = offset[1:3] + self.report_error(offset[0], offset[1], + 'E901 %s: %s' % (exc_type.__name__, exc.args[0]), + self.generate_tokens) + generate_tokens.__doc__ = " Check if the syntax is valid." + + def check_all(self, expected=None, line_offset=0): + """ + Run all checks on the input file. + """ + self.report.init_file(self.filename, self.lines, expected, line_offset) + self.line_number = 0 + self.indent_char = None + self.indent_level = 0 + self.previous_logical = '' + self.tokens = [] + self.blank_lines = blank_lines_before_comment = 0 + parens = 0 + for token in self.generate_tokens(): + self.tokens.append(token) + token_type, text = token[0:2] + if self.verbose >= 3: + if token[2][0] == token[3][0]: + pos = '[%s:%s]' % (token[2][1] or '', token[3][1]) + else: + pos = 'l.%s' % token[3][0] + print(('l.%s\t%s\t%s\t%r' % + (token[2][0], pos, tokenize.tok_name[token[0]], text))) + if token_type == tokenize.COMMENT or token_type == tokenize.STRING: + for sre in re.finditer(r"[:.;,] ?[A-Za-z]", text): + pos = sre.span()[0] + part = text[:pos] + line = token[2][0] + part.count('\n') + offset = 0 if part.count('\n') > 0 else token[2][1] + col = offset + pos - part.rfind('\n') + 1 + if sre.group(0)[0] == '.': + msg = ('E289 Too many spaces after period. ' + 'Use only one.') + self.report_error(line, col, msg, check=None) + elif sre.group(0)[0] == ',': + msg = 'E288 Too many spaces after comma. Use only one.' + self.report_error(line, col, msg, check=None) + else: + msg = ('E287 Too many spaces after punctuation. ' + 'Use only one.') + self.report_error(line, col, msg, check=None) + if token_type == tokenize.OP: + if text in '([{': + parens += 1 + elif text in '}])': + parens -= 1 + elif not parens: + if token_type == tokenize.NEWLINE: + if self.blank_lines < blank_lines_before_comment: + self.blank_lines = blank_lines_before_comment + self.check_logical() + self.tokens = [] + self.blank_lines = blank_lines_before_comment = 0 + elif token_type == tokenize.NL: + if len(self.tokens) == 1: + # The physical line contains only this token. + self.blank_lines += 1 + self.tokens = [] + elif token_type == tokenize.COMMENT and len(self.tokens) == 1: + if blank_lines_before_comment < self.blank_lines: + blank_lines_before_comment = self.blank_lines + self.blank_lines = 0 + if COMMENT_WITH_NL: + # The comment also ends a physical line + self.tokens = [] + if self.blank_lines > 1: + msg = 'E389 File ends in multiple blank lines' + self.report_error(token[2][0], 0, msg, check=None) + + return self.report.get_file_results() + + +class BaseReport: + """Collect the results of the checks.""" + print_filename = False + + def __init__(self, options): + self._benchmark_keys = options.benchmark_keys + self._ignore_code = options.ignore_code + # Results + self.elapsed = 0 + self.total_errors = 0 + self.counters = dict.fromkeys(self._benchmark_keys, 0) + self.messages = {} + + def start(self): + """Start the timer.""" + self._start_time = time.time() + + def stop(self): + """Stop the timer.""" + self.elapsed = time.time() - self._start_time + + def init_file(self, filename, lines, expected, line_offset): + """Signal a new file.""" + self.filename = filename + self.lines = lines + self.expected = expected or () + self.line_offset = line_offset + self.file_errors = 0 + self.counters['files'] += 1 + self.counters['physical lines'] += len(lines) + + def increment_logical_line(self): + """Signal a new logical line.""" + self.counters['logical lines'] += 1 + + def error(self, line_number, offset, text, check): + """Report an error, according to options.""" + code = text[:4] + if self._ignore_code(code): + return + if code in self.counters: + self.counters[code] += 1 + else: + self.counters[code] = 1 + self.messages[code] = text[5:] + # Don't care about expected errors or warnings + if code in self.expected: + return + if self.print_filename and not self.file_errors: + print((self.filename)) + self.file_errors += 1 + self.total_errors += 1 + return code + + def get_file_results(self): + """Return the count of errors and warnings for this file.""" + return self.file_errors + + def get_count(self, prefix=''): + """Return the total count of errors and warnings.""" + return sum([self.counters[key] + for key in self.messages if key.startswith(prefix)]) + + def get_statistics(self, prefix=''): + """ + Get statistics for message codes that start with the prefix. + + prefix='' matches all errors and warnings + prefix='E' matches all errors + prefix='W' matches all warnings + prefix='E4' matches all errors that have to do with imports + """ + return ['%-7s %s %s' % (self.counters[key], key, self.messages[key]) + for key in sorted(self.messages) if key.startswith(prefix)] + + def print_statistics(self, prefix=''): + """Print overall statistics (number of errors and warnings).""" + for line in self.get_statistics(prefix): + print(line) + + def print_benchmark(self): + """Print benchmark numbers.""" + print(('%-7.2f %s' % (self.elapsed, 'seconds elapsed'))) + if self.elapsed: + for key in self._benchmark_keys: + print(('%-7d %s per second (%d total)' % + (self.counters[key] / self.elapsed, key, + self.counters[key]))) + + +class FileReport(BaseReport): + print_filename = True + + +class StandardReport(BaseReport): + """Collect and print the results of the checks.""" + + def __init__(self, options): + super().__init__(options) + self._fmt = REPORT_FORMAT.get(options.format.lower(), + options.format) + self._repeat = options.repeat + self._show_source = options.show_source + self._show_pep8 = options.show_pep8 + + def error(self, line_number, offset, text, check): + """ + Report an error, according to options. + """ + code = super().error(line_number, offset, + text, check) + if code and (self.counters[code] == 1 or self._repeat): + print((self._fmt % { + 'path': self.filename, + 'row': self.line_offset + line_number, 'col': offset + 1, + 'code': code, 'text': text[5:], + })) + if self._show_source: + if line_number > len(self.lines): + line = '' + else: + line = self.lines[line_number - 1] + print((line.rstrip())) + print((' ' * offset + '^')) + if self._show_pep8 and check is not None: + print((check.__doc__.lstrip('\n').rstrip())) + return code + + +class DiffReport(StandardReport): + """Collect and print the results for the changed lines only.""" + + def __init__(self, options): + super().__init__(options) + self._selected = options.selected_lines + + def error(self, line_number, offset, text, check): + if line_number not in self._selected[self.filename]: + return + return super().error(line_number, offset, text, check) + + +class TestReport(StandardReport): + """Collect the results for the tests.""" + + def __init__(self, options): + options.benchmark_keys += ['test cases', 'failed tests'] + super().__init__(options) + self._verbose = options.verbose + + def get_file_results(self): + # Check if the expected errors were found + label = '%s:%s:1' % (self.filename, self.line_offset) + codes = sorted(self.expected) + for code in codes: + if not self.counters.get(code): + self.file_errors += 1 + self.total_errors += 1 + print(('%s: error %s not found' % (label, code))) + if self._verbose and not self.file_errors: + print(('%s: passed (%s)' % + (label, ' '.join(codes) or 'Okay'))) + self.counters['test cases'] += 1 + if self.file_errors: + self.counters['failed tests'] += 1 + # Reset counters + for key in set(self.counters) - set(self._benchmark_keys): + del self.counters[key] + self.messages = {} + return self.file_errors + + def print_results(self): + results = ("%(physical lines)d lines tested: %(files)d files, " + "%(test cases)d test cases%%s." % self.counters) + if self.total_errors: + print((results % ", %s failures" % self.total_errors)) + else: + print((results % "")) + print(("Test failed." if self.total_errors else "Test passed.")) + + +class StyleGuide: + """Initialize a PEP-8 instance with few options.""" + + def __init__(self, *args, **kwargs): + # build options from the command line + parse_argv = kwargs.pop('parse_argv', False) + config_file = kwargs.pop('config_file', None) + options, self.paths = process_options(parse_argv=parse_argv, + config_file=config_file) + if args or kwargs: + # build options from dict + options_dict = dict(*args, **kwargs) + options.__dict__.update(options_dict) + if 'paths' in options_dict: + self.paths = options_dict['paths'] + + self.runner = self.input_file + self.options = options + + if not options.reporter: + options.reporter = BaseReport if options.quiet else StandardReport + + for index, value in enumerate(options.exclude): + options.exclude[index] = value.rstrip('/') + # Ignore all checks which are not explicitly selected + options.select = tuple(options.select or ()) + options.ignore = tuple(options.ignore or options.select and ('',)) + options.benchmark_keys = BENCHMARK_KEYS[:] + options.ignore_code = self.ignore_code + options.physical_checks = self.get_checks('physical_line') + options.logical_checks = self.get_checks('logical_line') + self.init_report() + + def init_report(self, reporter=None): + """Initialize the report instance.""" + self.options.report = (reporter or self.options.reporter)(self.options) + return self.options.report + + def check_files(self, paths=None): + """Run all checks on the paths.""" + if paths is None: + paths = self.paths + report = self.options.report + runner = self.runner + report.start() + for path in paths: + if os.path.isdir(path): + self.input_dir(path) + elif not self.excluded(path): + runner(path) + report.stop() + return report + + def input_file(self, filename, lines=None, expected=None, line_offset=0): + """Run all checks on a Python source file.""" + if self.options.verbose: + print(('checking %s' % filename)) + fchecker = Checker(filename, lines=lines, options=self.options) + return fchecker.check_all(expected=expected, line_offset=line_offset) + + def input_dir(self, dirname): + """Check all files in this directory and all subdirectories.""" + dirname = dirname.rstrip('/') + if self.excluded(dirname): + return 0 + counters = self.options.report.counters + verbose = self.options.verbose + filepatterns = self.options.filename + runner = self.runner + for root, dirs, files in os.walk(dirname): + if verbose: + print(('directory ' + root)) + counters['directories'] += 1 + for subdir in sorted(dirs): + if self.excluded(subdir): + dirs.remove(subdir) + for filename in sorted(files): + # contain a pattern that matches? + if ((filename_match(filename, filepatterns) and + not self.excluded(filename))): + runner(os.path.join(root, filename)) + + def excluded(self, filename): + """ + Check if options.exclude contains a pattern that matches filename. + """ + basename = os.path.basename(filename) + return filename_match(basename, self.options.exclude, default=False) + + def ignore_code(self, code): + """ + Check if the error code should be ignored. + + If 'options.select' contains a prefix of the error code, + return False. Else, if 'options.ignore' contains a prefix of + the error code, return True. + """ + return (code.startswith(self.options.ignore) and + not code.startswith(self.options.select)) + + def get_checks(self, argument_name): + """ + Find all globally visible functions where the first argument name + starts with argument_name and which contain selected tests. + """ + checks = [] + for name, codes, function, args in find_checks(argument_name): + if any(not (code and self.ignore_code(code)) for code in codes): + checks.append((name, function, args)) + return sorted(checks) + + +def init_tests(pep8style): + """ + Initialize testing framework. + + A test file can provide many tests. Each test starts with a + declaration. This declaration is a single line starting with '#:'. + It declares codes of expected failures, separated by spaces or 'Okay' + if no failure is expected. + If the file does not contain such declaration, it should pass all + tests. If the declaration is empty, following lines are not checked, + until next declaration. + + Examples: + + * Only E224 and W701 are expected: #: E224 W701 + * Following example is conform: #: Okay + * Don't check these lines: #: + """ + report = pep8style.init_report(TestReport) + runner = pep8style.input_file + + def run_tests(filename): + """Run all the tests from a file.""" + lines = readlines(filename) + ['#:\n'] + line_offset = 0 + codes = ['Okay'] + testcase = [] + count_files = report.counters['files'] + for index, line in enumerate(lines): + if not line.startswith('#:'): + if codes: + # Collect the lines of the test case + testcase.append(line) + continue + if codes and index: + codes = [c for c in codes if c != 'Okay'] + # Run the checker + runner(filename, testcase, expected=codes, + line_offset=line_offset) + # output the real line numbers + line_offset = index + 1 + # configure the expected errors + codes = line.split()[1:] + # empty the test case buffer + del testcase[:] + report.counters['files'] = count_files + 1 + return report.counters['failed tests'] + + pep8style.runner = run_tests + + +def selftest(options): + """ + Test all check functions with test cases in docstrings. + """ + count_failed = count_all = 0 + report = BaseReport(options) + counters = report.counters + checks = options.physical_checks + options.logical_checks + for name, check, argument_names in checks: + for line in check.__doc__.splitlines(): + line = line.lstrip() + match = SELFTEST_REGEX.match(line) + if match is None: + continue + code, source = match.groups() + checker = Checker(None, options=options, report=report) + for part in source.split(r'\n'): + part = part.replace(r'\t', '\t') + part = part.replace(r'\s', ' ') + checker.lines.append(part + '\n') + checker.check_all() + error = None + if code == 'Okay': + if len(counters) > len(options.benchmark_keys): + codes = [key for key in counters + if key not in options.benchmark_keys] + error = "incorrectly found %s" % ', '.join(codes) + elif not counters.get(code): + error = "failed to find %s" % code + # Keep showing errors for multiple tests + for key in set(counters) - set(options.benchmark_keys): + del counters[key] + report.messages = {} + count_all += 1 + if not error: + if options.verbose: + print(("%s: %s" % (code, source))) + else: + count_failed += 1 + print(("%s: %s:" % (__file__, error))) + for line in checker.lines: + print((line.rstrip())) + return count_failed, count_all + + +def read_config(options, args, arglist, parser): + """Read both user configuration and local configuration.""" + config = RawConfigParser() + + user_conf = options.config + if user_conf and os.path.isfile(user_conf): + if options.verbose: + print(('user configuration: %s' % user_conf)) + config.read(user_conf) + + parent = tail = args and os.path.abspath(os.path.commonprefix(args)) + while tail: + local_conf = os.path.join(parent, '.pep8') + if os.path.isfile(local_conf): + if options.verbose: + print(('local configuration: %s' % local_conf)) + config.read(local_conf) + break + parent, tail = os.path.split(parent) + + if config.has_section('pep8'): + option_list = dict([(o.dest, o.type or o.action) + for o in parser.option_list]) + + # First, read the default values + new_options, _ = parser.parse_args([]) + + # Second, parse the configuration + for opt in config.options('pep8'): + if options.verbose > 1: + print((' %s = %s' % (opt, config.get('pep8', opt)))) + if opt.replace('_', '-') not in parser.config_options: + print(('Unknown option: \'%s\'\n not in [%s]' % + (opt, ' '.join(parser.config_options)))) + sys.exit(1) + normalized_opt = opt.replace('-', '_') + opt_type = option_list[normalized_opt] + if opt_type in ('int', 'count'): + value = config.getint('pep8', opt) + elif opt_type == 'string': + value = config.get('pep8', opt) + else: + assert opt_type in ('store_true', 'store_false') + value = config.getboolean('pep8', opt) + setattr(new_options, normalized_opt, value) + + # Third, overwrite with the command-line options + options, _ = parser.parse_args(arglist, values=new_options) + + return options + + +def process_options(arglist=None, parse_argv=False, config_file=None): + """Process options passed either via arglist or via command line args.""" + if not arglist and not parse_argv: + # Don't read the command line if the module is used as a library. + arglist = [] + if config_file is True: + config_file = DEFAULT_CONFIG + parser = OptionParser(version=__version__, + usage="%prog [options] input ...") + parser.config_options = [ + 'exclude', 'filename', 'select', 'ignore', 'max-line-length', 'count', + 'format', 'quiet', 'show-pep8', 'show-source', 'statistics', 'verbose'] + parser.add_option('-v', '--verbose', default=0, action='count', + help="print status messages, or debug with -vv") + parser.add_option('-q', '--quiet', default=0, action='count', + help="report only file names, or nothing with -qq") + parser.add_option('-r', '--repeat', default=True, action='store_true', + help="(obsolete) show all occurrences of the same error") + parser.add_option('--first', action='store_false', dest='repeat', + help="show first occurrence of each error") + parser.add_option('--exclude', metavar='patterns', default=DEFAULT_EXCLUDE, + help="exclude files or directories which match these " + "comma separated patterns (default: %default)") + parser.add_option('--filename', metavar='patterns', default='*.py', + help="when parsing directories, only check filenames " + "matching these comma separated patterns " + "(default: %default)") + parser.add_option('--select', metavar='errors', default='', + help="select errors and warnings (e.g. E,W6)") + parser.add_option('--ignore', metavar='errors', default='', + help="skip errors and warnings (e.g. E4,W)") + parser.add_option('--show-source', action='store_true', + help="show source code for each error") + parser.add_option('--show-pep8', action='store_true', + help="show text of PEP 8 for each error " + "(implies --first)") + parser.add_option('--statistics', action='store_true', + help="count errors and warnings") + parser.add_option('--count', action='store_true', + help="print total number of errors and warnings " + "to standard error and set exit code to 1 if " + "total is not null") + parser.add_option('--max-line-length', type='int', metavar='n', + default=MAX_LINE_LENGTH, + help="set maximum allowed line length " + "(default: %default)") + parser.add_option('--format', metavar='format', default='default', + help="set the error format [default|pylint|]") + parser.add_option('--diff', action='store_true', + help="report only lines changed according to the " + "unified diff received on STDIN") + group = parser.add_option_group("Testing Options") + group.add_option('--testsuite', metavar='dir', + help="run regression tests from dir") + group.add_option('--doctest', action='store_true', + help="run doctest on myself") + group.add_option('--benchmark', action='store_true', + help="measure processing speed") + group = parser.add_option_group("Configuration", description=( + "The project options are read from the [pep8] section of the .pep8 " + "file located in any parent folder of the path(s) being processed. " + "Allowed options are: %s." % ', '.join(parser.config_options))) + group.add_option('--config', metavar='path', default=config_file, + help="config file location (default: %default)") + + options, args = parser.parse_args(arglist) + options.reporter = None + + if options.testsuite: + args.append(options.testsuite) + elif not options.doctest: + if parse_argv and not args: + if os.path.exists('.pep8') or options.diff: + args = ['.'] + else: + parser.error('input not specified') + options = read_config(options, args, arglist, parser) + options.reporter = parse_argv and options.quiet == 1 and FileReport + + if options.filename: + options.filename = options.filename.split(',') + options.exclude = options.exclude.split(',') + if options.select: + options.select = options.select.split(',') + if options.ignore: + options.ignore = options.ignore.split(',') + elif not (options.select or + options.testsuite or options.doctest) and DEFAULT_IGNORE: + # The default choice: ignore controversial checks + # (for doctest and testsuite, all checks are required) + options.ignore = DEFAULT_IGNORE.split(',') + + if options.diff: + options.reporter = DiffReport + stdin = stdin_get_value() + options.selected_lines = parse_udiff(stdin, options.filename, args[0]) + args = sorted(options.selected_lines) + + return options, args + + +def _main(): + """Parse options and run checks on Python source.""" + pep8style = StyleGuide(parse_argv=True, config_file=True) + options = pep8style.options + if options.doctest: + import doctest + fail_d, done_d = doctest.testmod(report=False, verbose=options.verbose) + fail_s, done_s = selftest(options) + count_failed = fail_s + fail_d + if not options.quiet: + count_passed = done_d + done_s - count_failed + print(("%d passed and %d failed." % (count_passed, count_failed))) + print(("Test failed." if count_failed else "Test passed.")) + if count_failed: + sys.exit(1) + if options.testsuite: + init_tests(pep8style) + report = pep8style.check_files() + if options.statistics: + report.print_statistics() + if options.benchmark: + report.print_benchmark() + if options.testsuite and not options.quiet: + report.print_results() + if report.total_errors: + if options.count: + sys.stderr.write(str(report.total_errors) + '\n') + sys.exit(1) + + +if __name__ == '__main__': + _main() diff --git a/sbapp/plyer/tools/pep8checker/pep8base.html b/sbapp/plyer/tools/pep8checker/pep8base.html new file mode 100644 index 0000000..e69ca6f --- /dev/null +++ b/sbapp/plyer/tools/pep8checker/pep8base.html @@ -0,0 +1,70 @@ + + + Kivy Styleguide Check + + + +
+

Kivy Styleguide (PEP8) Check

+
+ diff --git a/sbapp/plyer/tools/pep8checker/pep8kivy.py b/sbapp/plyer/tools/pep8checker/pep8kivy.py new file mode 100644 index 0000000..60ec4e7 --- /dev/null +++ b/sbapp/plyer/tools/pep8checker/pep8kivy.py @@ -0,0 +1,116 @@ +import sys +from os import walk +from os.path import isdir, join, abspath, dirname +import pep8 +import time + +htmlmode = False + +pep8_ignores = ( + # continuation line does not distinguish itself from next logical line + 'E125', + 'E126', # continuation line over-indented for hanging indent + 'E127', # continuation line over-indented for visual indent + 'E128') # continuation line under-indented for visual indent + + +class KivyStyleChecker(pep8.Checker): + def __init__(self, filename): + pep8.Checker.__init__(self, filename, ignore=pep8_ignores) + + def report_error(self, line_number, offset, text, check): + if htmlmode is False: + return pep8.Checker.report_error( + self, line_number, + offset, text, check + ) + + # html generation + print('{0}{1}'.format(line_number, text)) + + +if __name__ == '__main__': + + def usage(): + print('Usage: python pep8kivy.py [-html] *') + print('Folders will be checked recursively.') + sys.exit(1) + + if len(sys.argv) < 2: + usage() + if sys.argv[1] == '-html': + if len(sys.argv) < 3: + usage() + else: + htmlmode = True + targets = sys.argv[-1].split() + elif sys.argv == 2: + targets = sys.argv[-1] + else: + targets = sys.argv[-1].split() + + def check(fn): + try: + checker = KivyStyleChecker(fn) + except IOError: + # File couldn't be opened, so was deleted apparently. + # Don't check deleted files. + return 0 + return checker.check_all() + + errors = 0 + exclude_dirs = ['/lib', '/coverage', '/pep8', '/doc'] + exclude_files = ['kivy/gesture.py', 'osx/build.py', 'win32/build.py', + 'kivy/tools/stub-gl-debug.py', + 'kivy/modules/webdebugger.py', + 'kivy/modules/_webdebugger.py'] + for target in targets: + if isdir(target): + if htmlmode: + path = join(dirname(abspath(__file__)), 'pep8base.html') + print(open(path, 'r').read()) + print( + '''

Generated: %s

''' + '' % (time.strftime('%c')) + ) + + for dirpath, dirnames, filenames in walk(target): + cont = False + for pat in exclude_dirs: + if pat in dirpath: + cont = True + break + if cont: + continue + for filename in filenames: + if not filename.endswith('.py'): + continue + cont = False + complete_filename = join(dirpath, filename) + for pat in exclude_files: + if complete_filename.endswith(pat): + cont = True + if cont: + continue + + if htmlmode: + print( + '' + '' % complete_filename + ) + errors += check(complete_filename) + + if htmlmode: + print('
%s
') + + else: + # Got a single file to check + for pat in exclude_dirs + exclude_files: + if pat in target: + break + else: + if target.endswith('.py'): + errors += check(target) + + # If errors is 0 we return with 0. That's just fine. + sys.exit(errors) diff --git a/sbapp/plyer/tools/pep8checker/pre-commit.githook b/sbapp/plyer/tools/pep8checker/pre-commit.githook new file mode 100755 index 0000000..23d119c --- /dev/null +++ b/sbapp/plyer/tools/pep8checker/pre-commit.githook @@ -0,0 +1,78 @@ +#!/usr/bin/env python + +''' + Kivy Git Pre-Commit Hook to Enforce Styleguide + ============================================== + + This script is not supposed to be run directly. + Instead, copy it to your kivy/.git/hooks/ directory, call it 'pre-commit' + and make it executable. + + If you attempt to commit, git will run this script, which in turn will run + the styleguide checker over your code and abort the commit if there are any + errors. If that happens, please fix & retry. + + To install:: + + cp kivy/tools/pep8checker/pre-commit.githook .git/hooks/pre-commit + chmod +x .git/hooks/pre-commit +''' + +import sys, os +from os.path import dirname, abspath, sep, join +from subprocess import call, Popen, PIPE + +curdir = dirname(abspath(__file__)) +kivydir = sep.join(curdir.split(sep)[:-2]) +srcdir = join(kivydir, 'kivy') +script = join(srcdir, 'tools', 'pep8checker', 'pep8kivy.py') +try: + with open(script): pass +except IOError: + # if this not the kivy project, find the script file in the kivy project + os.environ['KIVY_NO_CONSOLELOG'] = '1' + import kivy + script = join(dirname(kivy.__file__), 'tools', 'pep8checker', 'pep8kivy.py') + srcdir = '' + +# Only check the files that were staged +#proc = Popen(['git', 'diff', '--cached', '--name-only', 'HEAD'], stdout=PIPE) +#targets = [join(kivydir, target) for target in proc.stdout] + +# Correction: only check the files that were staged, but do not include +# deleted files. +proc = Popen(['git', 'diff', '--cached', '--name-status', 'HEAD'], stdout=PIPE) +proc.wait() + +# This gives output like the following: +# +# A examples/widgets/lists/list_simple_in_kv.py +# A examples/widgets/lists/list_simple_in_kv_2.py +# D kivy/uix/observerview.py +# +# So check for D entries and remove them from targets. +# +targets = [] +for target in proc.stdout: + parts = [p.strip() for p in target.split()] + if parts[0] != 'D': + targets.append(join(kivydir, target.decode(encoding='UTF-8'))) + +# Untested possibility: After making the changes above for removing deleted +# files from targets, saw also where the git diff call could be: +# +# git diff --cached --name-only --diff-filter=ACM +# (leaving off D) +# +# and we could then remove the special handling in python for targets above. + +call(['git', 'stash', 'save', '--keep-index', '--quiet']) +retval = call([sys.executable, script, srcdir] + targets) +call(['git', 'stash', 'pop', '--quiet']) + +if retval: + # There are styleguide violations + print("Error:", retval, "styleguide violation(s) encountered!") + print("Your commit has been aborted. Please fix the violations and retry.") + sys.exit(retval) + diff --git a/sbapp/plyer/utils.py b/sbapp/plyer/utils.py new file mode 100644 index 0000000..101763e --- /dev/null +++ b/sbapp/plyer/utils.py @@ -0,0 +1,290 @@ +''' +Utils +===== + +''' +__all__ = ('platform', 'reify', 'deprecated') + +from os import environ +from os import path +from sys import platform as _sys_platform +import sys + + +class Platform: + ''' + Refactored to class to allow module function to be replaced + with module variable. + ''' + + def __init__(self): + self._platform_ios = None + self._platform_android = None + + def __eq__(self, other): + return other == self._get_platform() + + def __ne__(self, other): + return other != self._get_platform() + + def __str__(self): + return self._get_platform() + + def __repr__(self): + return 'platform name: \'{platform}\' from: \n{instance}'.format( + platform=self._get_platform(), + instance=super().__repr__() + ) + + def __hash__(self): + return self._get_platform().__hash__() + + def _get_platform(self): + + if self._platform_android is None: + # sys.getandroidapilevel is defined as of Python 3.7 + # ANDROID_ARGUMENT and ANDROID_PRIVATE are 2 environment variables + # from python-for-android project + self._platform_android = hasattr(sys, 'getandroidapilevel') or \ + 'ANDROID_ARGUMENT' in environ + + if self._platform_ios is None: + self._platform_ios = (environ.get('KIVY_BUILD', '') == 'ios') + + # On android, _sys_platform return 'linux2', so prefer to check the + # import of Android module than trying to rely on _sys_platform. + + if self._platform_android is True: + return 'android' + elif self._platform_ios is True: + return 'ios' + elif _sys_platform in ('win32', 'cygwin'): + return 'win' + elif _sys_platform == 'darwin': + return 'macosx' + elif _sys_platform[:5] == 'linux': + return 'linux' + return 'unknown' + + +platform = Platform() + + +class Proxy: + ''' + Based on http://code.activestate.com/recipes/496741-object-proxying + version by Tomer Filiba, PSF license. + ''' + + __slots__ = ['_obj', '_name', '_facade'] + + def __init__(self, name, facade): + object.__init__(self) + object.__setattr__(self, '_obj', None) + object.__setattr__(self, '_name', name) + object.__setattr__(self, '_facade', facade) + + def _ensure_obj(self): + obj = object.__getattribute__(self, '_obj') + if obj: + return obj + # do the import + try: + name = object.__getattribute__(self, '_name') + module = 'plyer.platforms.{}.{}'.format( + platform, name) + mod = __import__(module, fromlist='.') + obj = mod.instance() + except Exception: + import traceback + traceback.print_exc() + facade = object.__getattribute__(self, '_facade') + obj = facade() + + object.__setattr__(self, '_obj', obj) + return obj + + def __getattribute__(self, name): + result = None + + if name == '__doc__': + return result + + # run _ensure_obj func, result in _obj + object.__getattribute__(self, '_ensure_obj')() + + # return either Proxy instance or platform-dependent implementation + result = getattr(object.__getattribute__(self, '_obj'), name) + return result + + def __delattr__(self, name): + object.__getattribute__(self, '_ensure_obj')() + delattr(object.__getattribute__(self, '_obj'), name) + + def __setattr__(self, name, value): + object.__getattribute__(self, '_ensure_obj')() + setattr(object.__getattribute__(self, '_obj'), name, value) + + def __bool__(self): + object.__getattribute__(self, '_ensure_obj')() + return bool(object.__getattribute__(self, '_obj')) + + def __str__(self): + object.__getattribute__(self, '_ensure_obj')() + return str(object.__getattribute__(self, '_obj')) + + def __repr__(self): + object.__getattribute__(self, '_ensure_obj')() + return repr(object.__getattribute__(self, '_obj')) + + +def whereis_exe(program): + ''' Tries to find the program on the system path. + Returns the path if it is found or None if it's not found. + ''' + path_split = ';' if platform == 'win' else ':' + for pth in environ.get('PATH', '').split(path_split): + folder = path.isdir(path.join(pth, program)) + available = path.exists(path.join(pth, program)) + if available and not folder: + return path.join(pth, program) + return None + + +class reify: + ''' + Put the result of a method which uses this (non-data) descriptor decorator + in the instance dict after the first call, effectively replacing the + decorator with an instance variable. + + It acts like @property, except that the function is only ever called once; + after that, the value is cached as a regular attribute. This gives you lazy + attribute creation on objects that are meant to be immutable. + + Taken from the `Pyramid project `_. + + To use this as a decorator:: + + @reify + def lazy(self): + ... + return hard_to_compute_int + first_time = self.lazy # lazy is reify obj, reify.__get__() runs + second_time = self.lazy # lazy is hard_to_compute_int + ''' + + def __init__(self, func): + self.func = func + self.__doc__ = func.__doc__ + + def __get__(self, inst, cls): + if inst is None: + return self + retval = self.func(inst) + setattr(inst, self.func.__name__, retval) + return retval + + +def deprecated(obj): + ''' + This is a decorator which can be used to mark functions and classes as + deprecated. It will result in a warning being emitted when a deprecated + function is called or a new instance of a class created. + + In case of classes, the warning is emitted before the __new__ method + of the decorated class is called, therefore a way before the __init__ + method itself. + ''' + + import warnings + from inspect import stack + from functools import wraps + from types import FunctionType, MethodType + + new_obj = None + + # wrap a function into a function emitting a deprecated warning + if isinstance(obj, FunctionType): + + @wraps(obj) + def new_func(*args, **kwargs): + # get the previous stack frame and extract file, line and caller + # stack() -> caller() + call_file, call_line, caller = stack()[1][1:4] + + # assemble warning + warning = ( + 'Call to deprecated function {} in {} line {}. ' + 'Called from {} line {}' + ' by {}().\n'.format( + obj.__name__, + obj.__code__.co_filename, + obj.__code__.co_firstlineno + 1, + call_file, call_line, caller + ) + ) + + warnings.warn('[{}] {}'.format('WARNING', warning)) + + # if there is a docstring present, emit docstring too + if obj.__doc__: + warnings.warn(obj.__doc__) + + # return function wrapper + return obj(*args, **kwargs) + new_obj = new_func + + # wrap a class into a class emitting a deprecated warning + # obj is class, type(obj) is metaclass, metaclasses inherit from type + elif isinstance(type(obj), type): + # we have an access to the metaclass instance (class) and need to print + # the warning when a class instance (object) is created with __new__ + # i.e. when calling Class() + + def obj_new(cls, child, *args, **kwargs): + ''' + Custom metaclass instance's __new__ method with deprecated warning. + Calls the original __new__ method afterwards. + ''' + # get the previous stack frame and extract file, line and caller + # stack() -> caller() + call_file, call_line, caller = stack()[1][1:4] + loc_file = obj.__module__ + + warnings.warn( + '[{}] Creating an instance of a deprecated class {} in {}.' + ' Called from {} line {} by {}().\n'.format( + 'WARNING', obj.__name__, loc_file, + call_file, call_line, caller + ) + ) + + # if there is a docstring present, emit docstring too + if obj.__doc__: + warnings.warn(obj.__doc__) + + # make sure nothing silly gets into the function + assert obj is cls + + # we are creating a __new__ for a class that inherits from + # a deprecated class, therefore in this particular case + # MRO is (child, cls, object) > (cls, object) + if len(child.__mro__) > len(cls.__mro__): + assert cls is child.__mro__[1], (cls.__mro__, child.__mro__) + + # we are creating __new__ directly for the deprecated class + # therefore MRO is the same for parent and child class + elif len(child.__mro__) == len(cls.__mro__): + assert cls is child + + # return the class back with the extended __new__ method + return obj.__old_new__(child) + + # back up the old __new__ method and create an extended + # __new__ method that emits deprecated warnings + obj.__old_new__ = obj.__new__ + obj.__new__ = MethodType(obj_new, obj) + new_obj = obj + + # return a function wrapper or an extended class + return new_obj