mirror of
https://github.com/markqvist/Sideband.git
synced 2024-10-01 03:15:37 -04:00
Updated, modded and refactored plyer
This commit is contained in:
parent
16e055ba25
commit
bb86bee399
@ -13,11 +13,15 @@ __all__ = (
|
||||
'stt', 'temperature', 'tts', 'uniqueid', 'vibrator', 'wifi', 'devicename'
|
||||
)
|
||||
|
||||
__version__ = '2.1.0.dev0'
|
||||
__version__ = '2.2.0.dev0'
|
||||
|
||||
|
||||
from plyer import facades
|
||||
from plyer.utils import Proxy
|
||||
import RNS
|
||||
if RNS.vendor.platformutils.is_android():
|
||||
from plyer import facades
|
||||
from plyer.utils import Proxy
|
||||
else:
|
||||
from sbapp.plyer import facades
|
||||
from sbapp.plyer.utils import Proxy
|
||||
|
||||
#: Accelerometer proxy to :class:`plyer.facades.Accelerometer`
|
||||
accelerometer = Proxy('accelerometer', facades.Accelerometer)
|
||||
|
@ -14,38 +14,76 @@ __all__ = ('Accelerometer', 'Audio', 'Barometer', 'Battery', 'Call', 'Camera',
|
||||
'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
|
||||
import RNS
|
||||
if RNS.vendor.platformutils.is_android():
|
||||
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
|
||||
else:
|
||||
from sbapp.plyer.facades.accelerometer import Accelerometer
|
||||
from sbapp.plyer.facades.audio import Audio
|
||||
from sbapp.plyer.facades.barometer import Barometer
|
||||
from sbapp.plyer.facades.battery import Battery
|
||||
from sbapp.plyer.facades.call import Call
|
||||
from sbapp.plyer.facades.camera import Camera
|
||||
from sbapp.plyer.facades.compass import Compass
|
||||
from sbapp.plyer.facades.email import Email
|
||||
from sbapp.plyer.facades.filechooser import FileChooser
|
||||
from sbapp.plyer.facades.flash import Flash
|
||||
from sbapp.plyer.facades.gps import GPS
|
||||
from sbapp.plyer.facades.gravity import Gravity
|
||||
from sbapp.plyer.facades.gyroscope import Gyroscope
|
||||
from sbapp.plyer.facades.irblaster import IrBlaster
|
||||
from sbapp.plyer.facades.light import Light
|
||||
from sbapp.plyer.facades.proximity import Proximity
|
||||
from sbapp.plyer.facades.orientation import Orientation
|
||||
from sbapp.plyer.facades.notification import Notification
|
||||
from sbapp.plyer.facades.sms import Sms
|
||||
from sbapp.plyer.facades.stt import STT
|
||||
from sbapp.plyer.facades.tts import TTS
|
||||
from sbapp.plyer.facades.uniqueid import UniqueID
|
||||
from sbapp.plyer.facades.vibrator import Vibrator
|
||||
from sbapp.plyer.facades.wifi import Wifi
|
||||
from sbapp.plyer.facades.temperature import Temperature
|
||||
from sbapp.plyer.facades.humidity import Humidity
|
||||
from sbapp.plyer.facades.spatialorientation import SpatialOrientation
|
||||
from sbapp.plyer.facades.brightness import Brightness
|
||||
from sbapp.plyer.facades.keystore import Keystore
|
||||
from sbapp.plyer.facades.storagepath import StoragePath
|
||||
from sbapp.plyer.facades.bluetooth import Bluetooth
|
||||
from sbapp.plyer.facades.processors import Processors
|
||||
from sbapp.plyer.facades.cpu import CPU
|
||||
from sbapp.plyer.facades.screenshot import Screenshot
|
||||
from sbapp.plyer.facades.devicename import DeviceName
|
||||
|
@ -94,6 +94,7 @@ class Audio:
|
||||
# private
|
||||
|
||||
def _start(self):
|
||||
raise IOError("JUICE")
|
||||
raise NotImplementedError()
|
||||
|
||||
def _stop(self):
|
||||
|
@ -4,7 +4,7 @@ class 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
|
||||
|
88
sbapp/plyer/facades/maps.py
Normal file
88
sbapp/plyer/facades/maps.py
Normal file
@ -0,0 +1,88 @@
|
||||
'''
|
||||
Maps
|
||||
=======
|
||||
The :class:`Maps` creates a client for accessing the default Maps API.
|
||||
|
||||
Holds features such as opening a location by
|
||||
address & latitude/longitude, create queries, or find directions between
|
||||
two points
|
||||
|
||||
Simple Examples
|
||||
---------------
|
||||
|
||||
Perform a search::
|
||||
|
||||
>>> from plyer import maps
|
||||
>>> maps.search('Mexican Restaurant')
|
||||
>>> maps.search('Taco Bell', latitude=38.5810606, longitude=-121.493895)
|
||||
|
||||
Get directions to a location::
|
||||
|
||||
>>> from plyer import maps
|
||||
>>> maps.route('Cupertino', 'San Francisco')
|
||||
>>> maps.route('41.9156316,-72.6130726', '42.65228271484,-73.7577362060')
|
||||
|
||||
View a specific location::
|
||||
|
||||
>>> from plyer import maps
|
||||
>>> maps.open_by_address('25 Leshin Lane, Hightstown, NJ')
|
||||
>>> maps.open_by_lat_long(30.451468, -91.187149)
|
||||
>>> maps.open_by_lat_long(30.451468, -91.187149, name='Home')
|
||||
|
||||
Supported Platforms
|
||||
-------------------
|
||||
macOS, iOS
|
||||
---------------
|
||||
'''
|
||||
|
||||
|
||||
class Maps:
|
||||
'''
|
||||
Maps facade.
|
||||
'''
|
||||
|
||||
def open_by_address(self, address, **kwargs):
|
||||
'''
|
||||
Open the specificed location by address in the default Maps API
|
||||
'''
|
||||
self._open_by_address(address, **kwargs)
|
||||
|
||||
def open_by_lat_long(self, latitude, longitude, **kwargs):
|
||||
'''
|
||||
Open the specificed location by latitude & longitude coordinates
|
||||
in the default Maps API
|
||||
'''
|
||||
self._open_by_lat_long(latitude, longitude, **kwargs)
|
||||
|
||||
def search(self, query, **kwargs):
|
||||
'''
|
||||
The query. This parameter is treated as if its value had been typed
|
||||
into the Maps search field by the user.
|
||||
|
||||
Note that query=* is not supported
|
||||
'''
|
||||
self._search(query, **kwargs)
|
||||
|
||||
def route(self, saddr, daddr, **kwargs):
|
||||
'''
|
||||
To provide navigation directions from one location to another.
|
||||
|
||||
:param saddr: The source address to be used as the starting
|
||||
point for directions.
|
||||
|
||||
:param daddr: The destination address to be used as the
|
||||
destination point for directions.
|
||||
'''
|
||||
self._route(saddr, daddr, **kwargs)
|
||||
|
||||
def _open_by_address(self, address, **kwargs):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _open_by_lat_long(self, latitude, longitude, **kwargs):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _search(self, query, **kwargs):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _route(self, saddr, daddr, **kwargs):
|
||||
raise NotImplementedError()
|
@ -45,8 +45,8 @@ class Notification:
|
||||
Notification facade.
|
||||
'''
|
||||
|
||||
def notify(self, title='', message='', app_name='', app_icon='', notification_icon=None,
|
||||
timeout=10, ticker='', toast=False, hints={}, context_override=None):
|
||||
def notify(self, title='', message='', app_name='', app_icon='',
|
||||
timeout=10, ticker='', toast=False, hints={}):
|
||||
'''
|
||||
Send a notification.
|
||||
|
||||
@ -83,8 +83,8 @@ class Notification:
|
||||
|
||||
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, context_override=context_override
|
||||
app_icon=app_icon, app_name=app_name,
|
||||
timeout=timeout, ticker=ticker, toast=toast, hints=hints
|
||||
)
|
||||
|
||||
# private
|
||||
|
@ -30,7 +30,7 @@ To set sensor::
|
||||
|
||||
Supported Platforms
|
||||
-------------------
|
||||
Android
|
||||
Android, Linux
|
||||
|
||||
'''
|
||||
|
||||
|
@ -23,7 +23,7 @@ To send sms::
|
||||
|
||||
Supported Platforms
|
||||
-------------------
|
||||
Android, iOS
|
||||
Android, iOS, macOS
|
||||
|
||||
'''
|
||||
|
||||
@ -33,17 +33,23 @@ class Sms:
|
||||
Sms facade.
|
||||
'''
|
||||
|
||||
def send(self, recipient, message):
|
||||
def send(self, recipient, message, mode=None, **kwargs):
|
||||
'''
|
||||
Send SMS or open SMS interface.
|
||||
Includes optional `mode` parameter for macOS that can be set to
|
||||
`'SMS'` if carrier-activated device is correctly paired and
|
||||
configured to macOS.
|
||||
|
||||
:param recipient: The receiver
|
||||
:param message: the message
|
||||
:param mode: (optional, macOS only), can be set to 'iMessage'
|
||||
(default) or 'SMS'
|
||||
|
||||
:type recipient: number
|
||||
:type message: str
|
||||
:type mode: str
|
||||
'''
|
||||
self._send(recipient=recipient, message=message)
|
||||
self._send(recipient=recipient, message=message, mode=mode, **kwargs)
|
||||
|
||||
# private
|
||||
|
||||
|
@ -70,7 +70,7 @@ class AndroidAccelerometer(Accelerometer):
|
||||
return (None, None, None)
|
||||
|
||||
def __del__(self):
|
||||
if(self.bState):
|
||||
if self.bState:
|
||||
self._disable()
|
||||
super().__del__()
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
import time
|
||||
import threading
|
||||
from jnius import autoclass
|
||||
|
||||
from plyer.facades.audio import Audio
|
||||
@ -20,17 +22,31 @@ class AndroidAudio(Audio):
|
||||
'''
|
||||
|
||||
def __init__(self, file_path=None):
|
||||
default_path = '/sdcard/testrecorder.3gp'
|
||||
default_path = None
|
||||
super().__init__(file_path or default_path)
|
||||
|
||||
self._recorder = None
|
||||
self._player = None
|
||||
self._check_thread = None
|
||||
self._finished_callback = None
|
||||
|
||||
def _check_playback(self):
|
||||
while self._player and self._player.isPlaying():
|
||||
time.sleep(0.25)
|
||||
|
||||
if self._finished_callback and callable(self._finished_callback):
|
||||
self._check_thread = None
|
||||
self._finished_callback(self)
|
||||
|
||||
|
||||
def _start(self):
|
||||
self._recorder = MediaRecorder()
|
||||
self._recorder.setAudioSource(AudioSource.DEFAULT)
|
||||
self._recorder.setOutputFormat(OutputFormat.DEFAULT)
|
||||
self._recorder.setAudioEncoder(AudioEncoder.DEFAULT)
|
||||
self._recorder.setAudioSamplingRate(44100)
|
||||
self._recorder.setAudioEncodingBitRate(128000)
|
||||
self._recorder.setAudioChannels(1)
|
||||
self._recorder.setOutputFormat(OutputFormat.MPEG_4)
|
||||
self._recorder.setAudioEncoder(AudioEncoder.AAC)
|
||||
self._recorder.setOutputFile(self.file_path)
|
||||
|
||||
self._recorder.prepare()
|
||||
@ -53,6 +69,10 @@ class AndroidAudio(Audio):
|
||||
self._player.prepare()
|
||||
self._player.start()
|
||||
|
||||
self._check_thread = threading.Thread(target=self._check_playback, daemon=True)
|
||||
self._check_thread.start()
|
||||
|
||||
|
||||
|
||||
def instance():
|
||||
return AndroidAudio()
|
||||
|
@ -14,7 +14,7 @@ Uri = autoclass('android.net.Uri')
|
||||
class AndroidCamera(Camera):
|
||||
|
||||
def _take_picture(self, on_complete, filename=None):
|
||||
assert(on_complete is not 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)
|
||||
@ -26,7 +26,7 @@ class AndroidCamera(Camera):
|
||||
activity.startActivityForResult(intent, 0x123)
|
||||
|
||||
def _take_video(self, on_complete, filename=None):
|
||||
assert(on_complete is not 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)
|
||||
|
@ -110,7 +110,7 @@ class AndroidCompass(Compass):
|
||||
return (None, None, None, None, None, None)
|
||||
|
||||
def __del__(self):
|
||||
if(self.bState):
|
||||
if self.bState:
|
||||
self._disable()
|
||||
super().__del__()
|
||||
|
||||
|
@ -43,7 +43,7 @@ using that result will use an incorrect one i.e. the default value of
|
||||
.. versionadded:: 1.4.0
|
||||
'''
|
||||
|
||||
from os.path import join, basename
|
||||
from os.path import join
|
||||
from random import randint
|
||||
|
||||
from android import activity, mActivity
|
||||
@ -62,6 +62,8 @@ 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')
|
||||
Files = autoclass('android.provider.MediaStore$Files')
|
||||
FileOutputStream = autoclass('java.io.FileOutputStream')
|
||||
|
||||
|
||||
class AndroidFileChooser(FileChooser):
|
||||
@ -74,6 +76,7 @@ class AndroidFileChooser(FileChooser):
|
||||
|
||||
# filechooser activity <-> result pair identification
|
||||
select_code = None
|
||||
save_code = None
|
||||
|
||||
# default selection value
|
||||
selection = None
|
||||
@ -105,6 +108,7 @@ class AndroidFileChooser(FileChooser):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.select_code = randint(123456, 654321)
|
||||
self.save_code = randint(123456, 654321)
|
||||
self.selection = None
|
||||
|
||||
# bind a function for a response from filechooser activity
|
||||
@ -139,9 +143,11 @@ class AndroidFileChooser(FileChooser):
|
||||
|
||||
# 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:
|
||||
if (
|
||||
not self.selected_mime_type
|
||||
or not isinstance(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])
|
||||
@ -163,6 +169,38 @@ class AndroidFileChooser(FileChooser):
|
||||
self.select_code
|
||||
)
|
||||
|
||||
def _save_file(self, **kwargs):
|
||||
self._save_callback = kwargs.pop("callback")
|
||||
|
||||
title = kwargs.pop("title", None)
|
||||
|
||||
self.selected_mime_type = \
|
||||
kwargs.pop("filters")[0] if "filters" in kwargs else ""
|
||||
|
||||
file_intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
if (
|
||||
not self.selected_mime_type
|
||||
or not isinstance(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
|
||||
)
|
||||
|
||||
if title:
|
||||
file_intent.putExtra(Intent.EXTRA_TITLE, title)
|
||||
|
||||
mActivity.startActivityForResult(
|
||||
Intent.createChooser(file_intent, cast(
|
||||
'java.lang.CharSequence',
|
||||
String("FileChooser")
|
||||
)),
|
||||
self.save_code
|
||||
)
|
||||
|
||||
def _on_activity_result(self, request_code, result_code, data):
|
||||
'''
|
||||
Listener for ``android.app.Activity.onActivityResult()`` assigned
|
||||
@ -171,28 +209,41 @@ class AndroidFileChooser(FileChooser):
|
||||
.. versionadded:: 1.4.0
|
||||
'''
|
||||
|
||||
# not our response
|
||||
if request_code != self.select_code:
|
||||
# bad data
|
||||
if data is None:
|
||||
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()), ]
|
||||
if request_code == self.select_code:
|
||||
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)
|
||||
# return value to object
|
||||
self.selection = selection
|
||||
# return value via callback
|
||||
self._handle_selection(selection)
|
||||
|
||||
elif request_code == self.save_code:
|
||||
uri = data.getData()
|
||||
|
||||
with mActivity.getContentResolver().openFileDescriptor(
|
||||
uri, "w"
|
||||
) as pfd:
|
||||
with FileOutputStream(
|
||||
pfd.getFileDescriptor()
|
||||
) as fileOutputStream:
|
||||
# return value via callback
|
||||
self._save_callback(fileOutputStream)
|
||||
|
||||
@staticmethod
|
||||
def _handle_external_documents(uri):
|
||||
@ -206,28 +257,19 @@ class AndroidFileChooser(FileChooser):
|
||||
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()
|
||||
primary_storage = storagepath.get_external_storage_dir()
|
||||
sdcard_storage = storagepath.get_sdcard_dir()
|
||||
|
||||
# external (removable) SD card i.e. microSD
|
||||
external = storagepath.get_sdcard_dir()
|
||||
try:
|
||||
external_base = basename(external)
|
||||
except TypeError:
|
||||
external_base = basename(internal)
|
||||
directory = primary_storage
|
||||
|
||||
# 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
|
||||
if file_type == "primary":
|
||||
directory = primary_storage
|
||||
elif file_type == "home":
|
||||
sd_card = join(Environment.getExternalStorageDirectory(
|
||||
).getAbsolutePath(), Environment.DIRECTORY_DOCUMENTS)
|
||||
directory = join(primary_storage, Environment.DIRECTORY_DOCUMENTS)
|
||||
elif sdcard_storage and file_type in sdcard_storage:
|
||||
directory = sdcard_storage
|
||||
|
||||
return join(sd_card, file_name)
|
||||
return join(directory, file_name)
|
||||
|
||||
@staticmethod
|
||||
def _handle_media_documents(uri):
|
||||
@ -248,6 +290,11 @@ class AndroidFileChooser(FileChooser):
|
||||
uri = VMedia.EXTERNAL_CONTENT_URI
|
||||
elif file_type == 'audio':
|
||||
uri = AMedia.EXTERNAL_CONTENT_URI
|
||||
|
||||
# Other file type was selected (probably in the Documents folder)
|
||||
else:
|
||||
uri = Files.getContentUri("external")
|
||||
|
||||
return file_name, selection, uri
|
||||
|
||||
@staticmethod
|
||||
@ -279,6 +326,23 @@ class AndroidFileChooser(FileChooser):
|
||||
.. versionadded:: 1.4.0
|
||||
'''
|
||||
|
||||
try:
|
||||
download_dir = Environment.getExternalStoragePublicDirectory(
|
||||
Environment.DIRECTORY_DOWNLOADS
|
||||
).getPath()
|
||||
path = AndroidFileChooser._parse_content(
|
||||
uri=uri,
|
||||
projection=["_display_name"],
|
||||
selection=None,
|
||||
selection_args=None,
|
||||
sort_order=None,
|
||||
)
|
||||
return join(download_dir, path)
|
||||
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# known locations, differ between machines
|
||||
downloads = [
|
||||
'content://downloads/public_downloads',
|
||||
@ -441,6 +505,8 @@ class AndroidFileChooser(FileChooser):
|
||||
mode = kwargs.pop('mode', None)
|
||||
if mode == 'open':
|
||||
self._open_file(**kwargs)
|
||||
elif mode == 'save':
|
||||
self._save_file(**kwargs)
|
||||
|
||||
|
||||
def instance():
|
||||
|
@ -86,4 +86,4 @@ class AndroidGPS(GPS):
|
||||
|
||||
|
||||
def instance():
|
||||
return AndroidGPS()
|
||||
return AndroidGPS()
|
||||
|
@ -110,7 +110,7 @@ class AndroidGyroscope(Gyroscope):
|
||||
return (None, None, None, None, None, None)
|
||||
|
||||
def __del__(self):
|
||||
if(self.bState):
|
||||
if self.bState:
|
||||
self._disable()
|
||||
super().__del__()
|
||||
|
||||
|
@ -154,10 +154,15 @@ class AndroidNotification(Notification):
|
||||
notification_intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
notification_intent.setAction(Intent.ACTION_MAIN)
|
||||
notification_intent.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
if SDK_INT >= 23:
|
||||
# FLAG_IMMUTABLE added in SDK 23, required since SDK 31:
|
||||
pending_flags = PendingIntent.FLAG_IMMUTABLE
|
||||
else:
|
||||
pending_flags = 0
|
||||
|
||||
# get our application Activity
|
||||
pending_intent = PendingIntent.getActivity(
|
||||
app_context, 0, notification_intent, 0
|
||||
app_context, 0, notification_intent, pending_flags
|
||||
)
|
||||
|
||||
notification.setContentIntent(pending_intent)
|
||||
@ -179,8 +184,6 @@ class AndroidNotification(Notification):
|
||||
kwargs.get('title', '').encode('utf-8')
|
||||
)
|
||||
icon = kwargs.get('app_icon')
|
||||
notification_icon = kwargs.get('notification_icon')
|
||||
context_override = kwargs.get('context_override')
|
||||
|
||||
# decide whether toast only or proper notification
|
||||
if kwargs.get('toast'):
|
||||
|
@ -3,14 +3,13 @@ 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 plyer.platforms.android import SDK_INT
|
||||
from jnius import autoclass, cast
|
||||
from android import mActivity
|
||||
|
||||
Environment = autoclass('android.os.Environment')
|
||||
Context = autoclass('android.content.Context')
|
||||
Environment = autoclass("android.os.Environment")
|
||||
Context = autoclass("android.content.Context")
|
||||
|
||||
|
||||
class AndroidStoragePath(StoragePath):
|
||||
@ -25,17 +24,29 @@ class AndroidStoragePath(StoragePath):
|
||||
'''
|
||||
.. 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
|
||||
context = mActivity.getApplicationContext()
|
||||
storage_manager = cast(
|
||||
"android.os.storage.StorageManager",
|
||||
context.getSystemService(Context.STORAGE_SERVICE),
|
||||
)
|
||||
|
||||
if storage_manager is not None:
|
||||
if SDK_INT >= 24:
|
||||
storage_volumes = storage_manager.getStorageVolumes()
|
||||
for storage_volume in storage_volumes:
|
||||
if storage_volume.isRemovable():
|
||||
try:
|
||||
directory = storage_volume.getDirectory()
|
||||
except AttributeError:
|
||||
directory = storage_volume.getPathFile()
|
||||
path = directory.getAbsolutePath()
|
||||
else:
|
||||
storage_volumes = storage_manager.getVolumeList()
|
||||
for storage_volume in storage_volumes:
|
||||
if storage_volume.isRemovable():
|
||||
path = storage_volume.getPath()
|
||||
|
||||
return path
|
||||
|
||||
def _get_root_dir(self):
|
||||
|
@ -6,7 +6,7 @@ Taken from: http://pyobjus.readthedocs.org/en/latest/pyobjus_ios.html \
|
||||
#accessing-accelerometer
|
||||
'''
|
||||
|
||||
from plyer.facades import Accelerometer
|
||||
from sbapp.plyer.facades import Accelerometer
|
||||
from pyobjus import autoclass
|
||||
|
||||
|
||||
|
@ -3,7 +3,7 @@ iOS Barometer
|
||||
-------------
|
||||
'''
|
||||
|
||||
from plyer.facades import Barometer
|
||||
from sbapp.plyer.facades import Barometer
|
||||
from pyobjus import autoclass
|
||||
|
||||
|
||||
|
@ -4,7 +4,7 @@ Module of iOS API for plyer.battery.
|
||||
|
||||
from pyobjus import autoclass
|
||||
from pyobjus.dylib_manager import load_framework
|
||||
from plyer.facades import Battery
|
||||
from sbapp.plyer.facades import Battery
|
||||
|
||||
load_framework('/System/Library/Frameworks/UIKit.framework')
|
||||
UIDevice = autoclass('UIDevice')
|
||||
|
@ -4,7 +4,7 @@ iOS Brightness
|
||||
'''
|
||||
|
||||
from pyobjus import autoclass
|
||||
from plyer.facades import Brightness
|
||||
from sbapp.plyer.facades import Brightness
|
||||
from pyobjus.dylib_manager import load_framework
|
||||
|
||||
load_framework('/System/Library/Frameworks/UIKit.framework')
|
||||
|
@ -3,7 +3,7 @@ IOS Call
|
||||
----------
|
||||
'''
|
||||
|
||||
from plyer.facades import Call
|
||||
from sbapp.plyer.facades import Call
|
||||
from pyobjus import autoclass, objc_str
|
||||
|
||||
NSURL = autoclass('NSURL')
|
||||
|
@ -1,7 +1,7 @@
|
||||
from os import remove
|
||||
from plyer.facades import Camera
|
||||
from sbapp.plyer.facades import Camera
|
||||
|
||||
from plyer.utils import reify
|
||||
from sbapp.plyer.utils import reify
|
||||
|
||||
|
||||
class iOSCamera(Camera):
|
||||
@ -14,7 +14,7 @@ class iOSCamera(Camera):
|
||||
return PhotosLibrary()
|
||||
|
||||
def _take_picture(self, on_complete, filename=None):
|
||||
assert(on_complete is not None)
|
||||
assert on_complete is not None
|
||||
self.on_complete = on_complete
|
||||
self.filename = filename
|
||||
photos = self.photos
|
||||
@ -38,7 +38,7 @@ class iOSCamera(Camera):
|
||||
self._remove(self.filename)
|
||||
|
||||
def _take_video(self, on_complete, filename=None):
|
||||
assert(on_complete is not None)
|
||||
assert on_complete is not None
|
||||
raise NotImplementedError
|
||||
|
||||
def _remove(self, fn):
|
||||
|
@ -3,7 +3,7 @@ iOS Compass
|
||||
-----------
|
||||
'''
|
||||
|
||||
from plyer.facades import Compass
|
||||
from sbapp.plyer.facades import Compass
|
||||
from pyobjus import autoclass
|
||||
|
||||
|
||||
|
@ -7,7 +7,7 @@ try:
|
||||
except ImportError:
|
||||
from urllib import quote
|
||||
|
||||
from plyer.facades import Email
|
||||
from sbapp.plyer.facades import Email
|
||||
from pyobjus import autoclass, objc_str
|
||||
from pyobjus.dylib_manager import load_framework
|
||||
|
||||
|
@ -7,7 +7,7 @@ This module houses the iOS implementation of the plyer FileChooser.
|
||||
.. versionadded:: 1.4.4
|
||||
'''
|
||||
|
||||
from plyer.facades import FileChooser
|
||||
from sbapp.plyer.facades import FileChooser
|
||||
from pyobjus import autoclass, protocol
|
||||
from pyobjus.dylib_manager import load_framework
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
Flash
|
||||
-----
|
||||
"""
|
||||
from plyer.facades import Flash
|
||||
from sbapp.plyer.facades import Flash
|
||||
from pyobjus import autoclass
|
||||
|
||||
NSString = autoclass("NSString")
|
||||
|
@ -5,7 +5,7 @@ iOS GPS
|
||||
|
||||
from pyobjus import autoclass, protocol
|
||||
from pyobjus.dylib_manager import load_framework
|
||||
from plyer.facades import GPS
|
||||
from sbapp.plyer.facades import GPS
|
||||
|
||||
load_framework('/System/Library/Frameworks/CoreLocation.framework')
|
||||
CLLocationManager = autoclass('CLLocationManager')
|
||||
|
@ -4,7 +4,7 @@ iOS Gravity
|
||||
|
||||
'''
|
||||
|
||||
from plyer.facades import Gravity
|
||||
from sbapp.plyer.facades import Gravity
|
||||
from pyobjus import autoclass
|
||||
|
||||
|
||||
|
@ -3,7 +3,7 @@ iOS Gyroscope
|
||||
---------------------
|
||||
'''
|
||||
|
||||
from plyer.facades import Gyroscope
|
||||
from sbapp.plyer.facades import Gyroscope
|
||||
from pyobjus import autoclass
|
||||
|
||||
from pyobjus.dylib_manager import load_framework
|
||||
|
@ -1,4 +1,4 @@
|
||||
from plyer.facades import Keystore
|
||||
from sbapp.plyer.facades import Keystore
|
||||
from pyobjus import autoclass, objc_str
|
||||
|
||||
NSUserDefaults = autoclass('NSUserDefaults')
|
||||
|
78
sbapp/plyer/platforms/ios/maps.py
Normal file
78
sbapp/plyer/platforms/ios/maps.py
Normal file
@ -0,0 +1,78 @@
|
||||
'''
|
||||
Module of iOS API for plyer.maps.
|
||||
'''
|
||||
|
||||
import webbrowser
|
||||
from sbapp.plyer.facades import Maps
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
|
||||
class iOSMaps(Maps):
|
||||
'''
|
||||
Implementation of iOS Maps API.
|
||||
'''
|
||||
|
||||
def _open_by_address(self, address, **kwargs):
|
||||
'''
|
||||
:param address: An address string that geolocation can understand.
|
||||
'''
|
||||
|
||||
address = quote_plus(address, safe=',')
|
||||
maps_address = 'http://maps.apple.com/?address=' + address
|
||||
|
||||
webbrowser.open(maps_address)
|
||||
|
||||
def _open_by_lat_long(self, latitude, longitude, **kwargs):
|
||||
'''
|
||||
Open a coordinate span denoting a latitudinal delta and a
|
||||
longitudinal delta (similar to MKCoordinateSpan)
|
||||
|
||||
:param name: (optional), will set the name of the dropped pin
|
||||
'''
|
||||
|
||||
name = kwargs.get("name", "Selected Location")
|
||||
maps_address = 'http://maps.apple.com/?ll={},{}&q={}'.format(
|
||||
latitude, longitude, name)
|
||||
|
||||
webbrowser.open(maps_address)
|
||||
|
||||
def _search(self, query, **kwargs):
|
||||
'''
|
||||
:param query: A string that describes the search object (ex. "Pizza")
|
||||
|
||||
:param latitude: (optional), narrow down query within area,
|
||||
MUST BE USED WITH LONGITUDE
|
||||
|
||||
:param longitude: (optional), narrow down query within area,
|
||||
MUST BE USED WITH LATITUDE
|
||||
'''
|
||||
|
||||
latitude = kwargs.get('latitude')
|
||||
longitude = kwargs.get('longitude')
|
||||
|
||||
query = quote_plus(query, safe=',')
|
||||
maps_address = 'http://maps.apple.com/?q=' + query
|
||||
|
||||
if latitude is not None and longitude is not None:
|
||||
maps_address += '&sll={},{}'.format(latitude, longitude)
|
||||
|
||||
webbrowser.open(maps_address)
|
||||
|
||||
def _route(self, saddr, daddr, **kwargs):
|
||||
'''
|
||||
:param saddr: can be given as 'address' or 'lat,long'
|
||||
:param daddr: can be given as 'address' or 'lat,long'
|
||||
'''
|
||||
saddr = quote_plus(saddr, safe=',')
|
||||
daddr = quote_plus(daddr, safe=',')
|
||||
|
||||
maps_address = 'http://maps.apple.com/?saddr={}&daddr={}'.format(
|
||||
saddr, daddr)
|
||||
webbrowser.open(maps_address)
|
||||
|
||||
|
||||
def instance():
|
||||
'''
|
||||
Instance for facade proxy.
|
||||
'''
|
||||
return iOSMaps()
|
@ -3,7 +3,7 @@ IOS Sms
|
||||
----------
|
||||
'''
|
||||
|
||||
from plyer.facades import Sms
|
||||
from sbapp.plyer.facades import Sms
|
||||
from pyobjus import autoclass, objc_str
|
||||
from pyobjus.dylib_manager import load_framework
|
||||
|
||||
|
@ -4,7 +4,7 @@ iOS Spatial Orientation
|
||||
|
||||
'''
|
||||
|
||||
from plyer.facades import SpatialOrientation
|
||||
from sbapp.plyer.facades import SpatialOrientation
|
||||
from pyobjus import autoclass
|
||||
|
||||
|
||||
|
@ -3,7 +3,7 @@ iOS Storage Path
|
||||
--------------------
|
||||
'''
|
||||
|
||||
from plyer.facades import StoragePath
|
||||
from sbapp.plyer.facades import StoragePath
|
||||
from pyobjus import autoclass
|
||||
import os
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
from pyobjus import autoclass, objc_str
|
||||
from pyobjus.dylib_manager import load_framework
|
||||
|
||||
from plyer.facades import TTS
|
||||
from sbapp.plyer.facades import TTS
|
||||
|
||||
load_framework('/System/Library/Frameworks/AVFoundation.framework')
|
||||
AVSpeechUtterance = autoclass('AVSpeechUtterance')
|
||||
@ -23,7 +23,7 @@ class iOSTextToSpeech(TTS):
|
||||
def _speak(self, **kwargs):
|
||||
message = kwargs.get('message')
|
||||
|
||||
if(not self.voice):
|
||||
if not self.voice:
|
||||
self._set_locale()
|
||||
|
||||
utterance = \
|
||||
|
@ -4,7 +4,7 @@ Module of iOS API for plyer.uniqueid.
|
||||
|
||||
from pyobjus import autoclass
|
||||
from pyobjus.dylib_manager import load_framework
|
||||
from plyer.facades import UniqueID
|
||||
from sbapp.plyer.facades import UniqueID
|
||||
|
||||
load_framework('/System/Library/Frameworks/UIKit.framework')
|
||||
UIDevice = autoclass('UIDevice')
|
||||
|
@ -4,7 +4,7 @@ Install: Add AudioToolbox framework to your application.
|
||||
'''
|
||||
|
||||
import ctypes
|
||||
from plyer.facades import Vibrator
|
||||
from sbapp.plyer.facades import Vibrator
|
||||
|
||||
|
||||
class IosVibrator(Vibrator):
|
||||
|
@ -3,7 +3,7 @@ Linux accelerometer
|
||||
---------------------
|
||||
'''
|
||||
|
||||
from plyer.facades import Accelerometer
|
||||
from sbapp.plyer.facades import Accelerometer
|
||||
import glob
|
||||
import re
|
||||
|
||||
|
48
sbapp/plyer/platforms/linux/audio.py
Normal file
48
sbapp/plyer/platforms/linux/audio.py
Normal file
@ -0,0 +1,48 @@
|
||||
import time
|
||||
import threading
|
||||
from sbapp.plyer.facades.audio import Audio
|
||||
|
||||
class LinuxAudio(Audio):
|
||||
|
||||
def __init__(self, file_path=None):
|
||||
default_path = None
|
||||
super().__init__(file_path or default_path)
|
||||
|
||||
self._recorder = None
|
||||
self._player = None
|
||||
self._check_thread = None
|
||||
self._finished_callback = None
|
||||
|
||||
def _check_playback(self):
|
||||
while self.is_playing:
|
||||
time.sleep(0.25)
|
||||
|
||||
if self._finished_callback and callable(self._finished_callback):
|
||||
self._check_thread = None
|
||||
self._finished_callback(self)
|
||||
|
||||
def _start(self):
|
||||
# TODO: Implement recording
|
||||
pass
|
||||
|
||||
def _stop(self):
|
||||
# TODO: Implement recording
|
||||
pass
|
||||
|
||||
def _play(self):
|
||||
# TODO: Implement playback
|
||||
self.is_playing = True
|
||||
|
||||
self._check_thread = threading.Thread(target=self._check_playback, daemon=True)
|
||||
self._check_thread.start()
|
||||
|
||||
def fauxplay():
|
||||
time.sleep(1.5)
|
||||
self.is_playing = False
|
||||
|
||||
threading.Thread(target=fauxplay, daemon=True).start()
|
||||
|
||||
|
||||
|
||||
def instance():
|
||||
return LinuxAudio()
|
@ -2,13 +2,12 @@
|
||||
Module of Linux API for plyer.battery.
|
||||
'''
|
||||
|
||||
import os
|
||||
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
|
||||
from sbapp.plyer.facades import Battery
|
||||
from sbapp.plyer.utils import whereis_exe
|
||||
|
||||
|
||||
class LinuxBattery(Battery):
|
||||
@ -20,10 +19,10 @@ class LinuxBattery(Battery):
|
||||
def _get_state(self):
|
||||
status = {"isCharging": None, "percentage": None}
|
||||
|
||||
kernel_bat_path = join('/sys', 'class', 'power_supply', self.node_name)
|
||||
kernel_bat_path = join('/sys', 'class', 'power_supply', 'BAT0')
|
||||
uevent = join(kernel_bat_path, 'uevent')
|
||||
|
||||
with open(uevent, "rb") as fle:
|
||||
with open(uevent) as fle:
|
||||
lines = [
|
||||
line.decode('utf-8').strip()
|
||||
for line in fle.readlines()
|
||||
@ -34,34 +33,70 @@ class LinuxBattery(Battery):
|
||||
}
|
||||
|
||||
is_charging = output['POWER_SUPPLY_STATUS'] == 'Charging'
|
||||
charge_percent = float(output['POWER_SUPPLY_CAPACITY'])
|
||||
total = float(output['POWER_SUPPLY_CHARGE_FULL'])
|
||||
now = float(output['POWER_SUPPLY_CHARGE_NOW'])
|
||||
|
||||
status['percentage'] = charge_percent
|
||||
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.")
|
||||
|
||||
node_exists = False
|
||||
bn = 0
|
||||
node_name = None
|
||||
for bi in range(0,10):
|
||||
path = join('/sys', 'class', 'power_supply', 'BAT'+str(bi))
|
||||
if os.path.isdir(path):
|
||||
node_name = "BAT"+str(bi)
|
||||
break
|
||||
|
||||
if node_name:
|
||||
b = LinuxBattery()
|
||||
b.node_name = node_name
|
||||
return b
|
||||
if whereis_exe('upower'):
|
||||
return UPowerBattery()
|
||||
sys.stderr.write("upower not found.")
|
||||
|
||||
if exists(join('/sys', 'class', 'power_supply', 'BAT0')):
|
||||
return LinuxBattery()
|
||||
return Battery()
|
||||
|
@ -4,7 +4,7 @@ Linux Brightness
|
||||
|
||||
'''
|
||||
|
||||
from plyer.facades import Brightness
|
||||
from sbapp.plyer.facades import Brightness
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
|
@ -5,8 +5,8 @@ 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
|
||||
from sbapp.plyer.facades import CPU
|
||||
from sbapp.plyer.utils import whereis_exe
|
||||
|
||||
|
||||
class LinuxCPU(CPU):
|
||||
|
@ -3,7 +3,7 @@ Module of Linux API for plyer.devicename.
|
||||
'''
|
||||
|
||||
import socket
|
||||
from plyer.facades import DeviceName
|
||||
from sbapp.plyer.facades import DeviceName
|
||||
|
||||
|
||||
class LinuxDeviceName(DeviceName):
|
||||
|
@ -7,8 +7,8 @@ try:
|
||||
from urllib.parse import quote
|
||||
except ImportError:
|
||||
from urllib import quote
|
||||
from plyer.facades import Email
|
||||
from plyer.utils import whereis_exe
|
||||
from sbapp.plyer.facades import Email
|
||||
from sbapp.plyer.utils import whereis_exe
|
||||
|
||||
|
||||
class LinuxEmail(Email):
|
||||
|
@ -3,7 +3,7 @@ Linux file chooser
|
||||
------------------
|
||||
'''
|
||||
|
||||
from plyer.facades import FileChooser
|
||||
from sbapp.plyer.facades import FileChooser
|
||||
from distutils.spawn import find_executable as which
|
||||
import os
|
||||
import subprocess as sp
|
||||
@ -122,7 +122,7 @@ class ZenityFileChooser(SubprocessFileChooser):
|
||||
if self.icon:
|
||||
cmdline += ["--window-icon", self.icon]
|
||||
for f in self.filters:
|
||||
if type(f) == str:
|
||||
if isinstance(f, str):
|
||||
cmdline += ["--file-filter", f]
|
||||
else:
|
||||
cmdline += [
|
||||
@ -150,7 +150,7 @@ class KDialogFileChooser(SubprocessFileChooser):
|
||||
filt = []
|
||||
|
||||
for f in self.filters:
|
||||
if type(f) == str:
|
||||
if isinstance(f, str):
|
||||
filt += [f]
|
||||
else:
|
||||
filt += list(f[1:])
|
||||
@ -195,7 +195,7 @@ class YADFileChooser(SubprocessFileChooser):
|
||||
def _gen_cmdline(self):
|
||||
cmdline = [
|
||||
which(self.executable),
|
||||
"--file-selection",
|
||||
"--file",
|
||||
"--confirm-overwrite",
|
||||
"--geometry",
|
||||
"800x600+150+150"
|
||||
@ -215,7 +215,7 @@ class YADFileChooser(SubprocessFileChooser):
|
||||
if self.icon:
|
||||
cmdline += ["--window-icon", self.icon]
|
||||
for f in self.filters:
|
||||
if type(f) == str:
|
||||
if isinstance(f, str):
|
||||
cmdline += ["--file-filter", f]
|
||||
else:
|
||||
cmdline += [
|
||||
|
@ -3,7 +3,7 @@ try:
|
||||
except ImportError:
|
||||
raise NotImplementedError()
|
||||
|
||||
from plyer.facades import Keystore
|
||||
from sbapp.plyer.facades import Keystore
|
||||
|
||||
|
||||
class LinuxKeystore(Keystore):
|
||||
|
@ -4,8 +4,8 @@ Module of Linux API for plyer.notification.
|
||||
|
||||
import warnings
|
||||
import subprocess
|
||||
from plyer.facades import Notification
|
||||
from plyer.utils import whereis_exe
|
||||
from sbapp.plyer.facades import Notification
|
||||
from sbapp.plyer.utils import whereis_exe
|
||||
import os
|
||||
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import subprocess as sb
|
||||
from plyer.facades import Orientation
|
||||
from sbapp.plyer.facades import Orientation
|
||||
|
||||
|
||||
class LinuxOrientation(Orientation):
|
||||
|
@ -1,6 +1,6 @@
|
||||
from subprocess import Popen, PIPE
|
||||
from plyer.facades import Processors
|
||||
from plyer.utils import whereis_exe
|
||||
from sbapp.plyer.facades import Processors
|
||||
from sbapp.plyer.utils import whereis_exe
|
||||
|
||||
from os import environ
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
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
|
||||
from sbapp.plyer.facades import Screenshot
|
||||
from sbapp.plyer.utils import whereis_exe
|
||||
from sbapp.plyer.platforms.linux.storagepath import LinuxStoragePath
|
||||
|
||||
|
||||
class LinuxScreenshot(Screenshot):
|
||||
|
@ -3,7 +3,7 @@ Linux Storage Path
|
||||
--------------------
|
||||
'''
|
||||
|
||||
from plyer.facades import StoragePath
|
||||
from sbapp.plyer.facades import StoragePath
|
||||
from os.path import expanduser, dirname, abspath, join, exists
|
||||
|
||||
# Default paths for each name
|
||||
|
@ -1,6 +1,6 @@
|
||||
import subprocess
|
||||
from plyer.facades import TTS
|
||||
from plyer.utils import whereis_exe
|
||||
from sbapp.plyer.facades import TTS
|
||||
from sbapp.plyer.utils import whereis_exe
|
||||
|
||||
|
||||
class EspeakTextToSpeech(TTS):
|
||||
|
@ -4,8 +4,8 @@ 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
|
||||
from sbapp.plyer.facades import UniqueID
|
||||
from sbapp.plyer.utils import whereis_exe
|
||||
|
||||
|
||||
class LinuxUniqueID(UniqueID):
|
||||
|
@ -6,8 +6,8 @@
|
||||
'''
|
||||
|
||||
from subprocess import Popen, PIPE, call
|
||||
from plyer.facades import Wifi
|
||||
from plyer.utils import whereis_exe, deprecated
|
||||
from sbapp.plyer.facades import Wifi
|
||||
from sbapp.plyer.utils import whereis_exe, deprecated
|
||||
|
||||
try:
|
||||
import wifi
|
||||
|
@ -3,8 +3,8 @@ MacOSX accelerometer
|
||||
---------------------
|
||||
'''
|
||||
|
||||
from plyer.facades import Accelerometer
|
||||
from plyer.platforms.macosx.libs import osx_motion_sensor
|
||||
from sbapp.plyer.facades import Accelerometer
|
||||
from sbapp.plyer.platforms.macosx.libs import osx_motion_sensor
|
||||
|
||||
|
||||
class OSXAccelerometer(Accelerometer):
|
||||
|
@ -3,8 +3,8 @@ 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
|
||||
from sbapp.plyer.facades import Audio
|
||||
from sbapp.plyer.platforms.macosx.storagepath import OSXStoragePath
|
||||
|
||||
load_framework(INCLUDE.Foundation)
|
||||
load_framework(INCLUDE.AVFoundation)
|
||||
@ -19,16 +19,24 @@ NSError = autoclass('NSError').alloc()
|
||||
|
||||
class OSXAudio(Audio):
|
||||
def __init__(self, file_path=None):
|
||||
default_path = join(
|
||||
OSXStoragePath().get_music_dir(),
|
||||
'audio.wav'
|
||||
)
|
||||
default_path = None
|
||||
super().__init__(file_path or default_path)
|
||||
|
||||
self._recorder = None
|
||||
self._player = None
|
||||
self._current_file = None
|
||||
|
||||
self._check_thread = None
|
||||
self._finished_callback = None
|
||||
|
||||
def _check_playback(self):
|
||||
while self._player and self._player.isPlaying:
|
||||
time.sleep(0.25)
|
||||
|
||||
if self._finished_callback and callable(self._finished_callback):
|
||||
self._check_thread = None
|
||||
self._finished_callback(self)
|
||||
|
||||
def _start(self):
|
||||
# Conversion of Python file path string to Objective-C NSString
|
||||
file_path_NSString = NSString.alloc()
|
||||
@ -44,7 +52,7 @@ class OSXAudio(Audio):
|
||||
# Internal audio file format specification
|
||||
af = AVAudioFormat.alloc()
|
||||
af = af.initWithCommonFormat_sampleRate_channels_interleaved_(
|
||||
1, 44100.0, 2, True
|
||||
1, 44100.0, 1, True
|
||||
)
|
||||
|
||||
# Audio recorder instance initialization with specified file NSURL
|
||||
@ -85,6 +93,9 @@ class OSXAudio(Audio):
|
||||
|
||||
self._player.play()
|
||||
|
||||
self._check_thread = threading.Thread(target=self._check_playback, daemon=True)
|
||||
self._check_thread.start()
|
||||
|
||||
|
||||
def instance():
|
||||
return OSXAudio()
|
||||
|
@ -4,8 +4,8 @@ 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
|
||||
from sbapp.plyer.facades import Battery
|
||||
from sbapp.plyer.utils import whereis_exe
|
||||
|
||||
|
||||
class OSXBattery(Battery):
|
||||
|
@ -3,8 +3,8 @@ Module of MacOS API for plyer.bluetooth.
|
||||
'''
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from plyer.facades import Bluetooth
|
||||
from plyer.utils import whereis_exe
|
||||
from sbapp.plyer.facades import Bluetooth
|
||||
from sbapp.plyer.utils import whereis_exe
|
||||
|
||||
from os import environ
|
||||
|
||||
|
@ -3,8 +3,8 @@ Module of MacOS API for plyer.cpu.
|
||||
'''
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from plyer.facades import CPU
|
||||
from plyer.utils import whereis_exe
|
||||
from sbapp.plyer.facades import CPU
|
||||
from sbapp.plyer.utils import whereis_exe
|
||||
|
||||
|
||||
class OSXCPU(CPU):
|
||||
|
@ -3,7 +3,7 @@ Module of MacOSX API for plyer.devicename.
|
||||
'''
|
||||
|
||||
import socket
|
||||
from plyer.facades import DeviceName
|
||||
from sbapp.plyer.facades import DeviceName
|
||||
|
||||
|
||||
class OSXDeviceName(DeviceName):
|
||||
|
@ -9,8 +9,8 @@ try:
|
||||
except ImportError:
|
||||
from urllib import quote
|
||||
|
||||
from plyer.facades import Email
|
||||
from plyer.utils import whereis_exe
|
||||
from sbapp.plyer.facades import Email
|
||||
from sbapp.plyer.utils import whereis_exe
|
||||
|
||||
|
||||
class MacOSXEmail(Email):
|
||||
|
@ -3,7 +3,7 @@ Mac OS X file chooser
|
||||
---------------------
|
||||
'''
|
||||
|
||||
from plyer.facades import FileChooser
|
||||
from sbapp.plyer.facades import FileChooser
|
||||
from pyobjus import autoclass, objc_arr, objc_str
|
||||
from pyobjus.dylib_manager import load_framework, INCLUDE
|
||||
|
||||
@ -80,7 +80,7 @@ class MacFileChooser:
|
||||
if self.filters:
|
||||
filthies = []
|
||||
for f in self.filters:
|
||||
if type(f) == str:
|
||||
if isinstance(f, str):
|
||||
f = (None, f)
|
||||
for s in f[1:]:
|
||||
if not self.use_extensions:
|
||||
|
@ -3,7 +3,7 @@ try:
|
||||
except ImportError:
|
||||
raise NotImplementedError()
|
||||
|
||||
from plyer.facades import Keystore
|
||||
from sbapp.plyer.facades import Keystore
|
||||
|
||||
|
||||
class OSXKeystore(Keystore):
|
||||
|
@ -86,7 +86,7 @@ def read_sms():
|
||||
inStructure = data_structure()
|
||||
outStructure = data_structure()
|
||||
|
||||
if(is_os_64bit() or hasattr(IOKit, 'IOConnectCallStructMethod')):
|
||||
if is_os_64bit() or hasattr(IOKit, 'IOConnectCallStructMethod'):
|
||||
structureInSize = IOItemCount(sizeof(data_structure))
|
||||
structureOutSize = c_size_t(sizeof(data_structure))
|
||||
|
||||
@ -120,7 +120,7 @@ def get_coord():
|
||||
ret, data = read_sms()
|
||||
|
||||
if (ret > 0):
|
||||
if(data.x):
|
||||
if data.x:
|
||||
return (data.x, data.y, data.z)
|
||||
else:
|
||||
return (None, None, None)
|
||||
|
90
sbapp/plyer/platforms/macosx/maps.py
Normal file
90
sbapp/plyer/platforms/macosx/maps.py
Normal file
@ -0,0 +1,90 @@
|
||||
'''
|
||||
Module of macOS API for plyer.maps.
|
||||
'''
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from sbapp.plyer.facades import Maps
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
|
||||
class MacOSMaps(Maps):
|
||||
'''
|
||||
Implementation of MacOS Maps API.
|
||||
'''
|
||||
|
||||
def _open_by_address(self, address, **kwargs):
|
||||
'''
|
||||
:param address: An address string that geolocation can understand.
|
||||
'''
|
||||
|
||||
address = quote_plus(address, safe=',')
|
||||
maps_address = 'http://maps.apple.com/?address=' + address
|
||||
|
||||
process = Popen(
|
||||
['open', '-a', 'Maps', maps_address],
|
||||
stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = process.communicate()
|
||||
|
||||
def _open_by_lat_long(self, latitude, longitude, **kwargs):
|
||||
'''
|
||||
Open a coordinate span denoting a latitudinal delta and a
|
||||
longitudinal delta (similar to MKCoordinateSpan)
|
||||
|
||||
:param name: (optional), will set the name of the dropped pin
|
||||
'''
|
||||
|
||||
name = kwargs.get("name", "Selected Location")
|
||||
maps_address = 'http://maps.apple.com/?ll={},{}&q={}'.format(
|
||||
latitude, longitude, name)
|
||||
|
||||
process = Popen(
|
||||
['open', '-a', 'Maps', maps_address],
|
||||
stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = process.communicate()
|
||||
|
||||
def _search(self, query, **kwargs):
|
||||
'''
|
||||
:param query: A string that describes the search object (ex. "Pizza")
|
||||
|
||||
:param latitude: (optional), narrow down query within area,
|
||||
MUST BE USED WITH LONGITUDE
|
||||
|
||||
:param longitude: (optional), narrow down query within area,
|
||||
MUST BE USED WITH LATITUDE
|
||||
'''
|
||||
|
||||
latitude = kwargs.get('latitude')
|
||||
longitude = kwargs.get('longitude')
|
||||
|
||||
query = quote_plus(query, safe=',')
|
||||
maps_address = 'http://maps.apple.com/?q=' + query
|
||||
|
||||
if latitude is not None and longitude is not None:
|
||||
maps_address += '&sll={},{}'.format(latitude, longitude)
|
||||
|
||||
process = Popen(
|
||||
['open', '-a', 'Maps', maps_address],
|
||||
stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = process.communicate()
|
||||
|
||||
def _route(self, saddr, daddr, **kwargs):
|
||||
'''
|
||||
:param saddr: can be given as 'address' or 'lat,long'
|
||||
:param daddr: can be given as 'address' or 'lat,long'
|
||||
'''
|
||||
saddr = quote_plus(saddr, safe=',')
|
||||
daddr = quote_plus(daddr, safe=',')
|
||||
|
||||
maps_address = 'http://maps.apple.com/?saddr={}&daddr={}'.format(
|
||||
saddr, daddr)
|
||||
process = Popen(
|
||||
['open', '-a', 'Maps', maps_address],
|
||||
stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = process.communicate()
|
||||
|
||||
|
||||
def instance():
|
||||
'''
|
||||
Instance for facade proxy.
|
||||
'''
|
||||
return MacOSMaps()
|
@ -2,7 +2,7 @@
|
||||
Module of MacOS API for plyer.notification.
|
||||
'''
|
||||
|
||||
from plyer.facades import Notification
|
||||
from sbapp.plyer.facades import Notification
|
||||
|
||||
from pyobjus import (
|
||||
autoclass, protocol, objc_str, ObjcBOOL
|
||||
|
@ -1,8 +1,8 @@
|
||||
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
|
||||
from sbapp.plyer.facades import Screenshot
|
||||
from sbapp.plyer.utils import whereis_exe
|
||||
from sbapp.plyer.platforms.macosx.storagepath import OSXStoragePath
|
||||
|
||||
|
||||
class OSXScreenshot(Screenshot):
|
||||
|
42
sbapp/plyer/platforms/macosx/sms.py
Normal file
42
sbapp/plyer/platforms/macosx/sms.py
Normal file
@ -0,0 +1,42 @@
|
||||
from subprocess import Popen, PIPE
|
||||
from sbapp.plyer.facades import Sms as SMS
|
||||
from sbapp.plyer.utils import whereis_exe
|
||||
|
||||
|
||||
class MacOSSMS(SMS):
|
||||
'''
|
||||
Implementation of macOS' Messages API
|
||||
'''
|
||||
|
||||
def _send(self, **kwargs):
|
||||
'''
|
||||
Will send `message` to `recipient` via Messages app
|
||||
|
||||
By default, if `mode` is not explicitly set, `iMessage` is used.
|
||||
In order to use `SMS` mode, a valid carrier-activated device must
|
||||
be connected and configured.
|
||||
'''
|
||||
|
||||
recipient = kwargs.get('recipient')
|
||||
message = kwargs.get('message')
|
||||
mode = kwargs.get('mode') # Supported modes: iMessage (default), SMS
|
||||
if not mode:
|
||||
mode = 'iMessage'
|
||||
|
||||
APPLESCRIPT = f"""tell application "Messages"
|
||||
set targetService to 1st account whose service type = {mode}
|
||||
set targetBuddy to participant "{recipient}" of targetService
|
||||
send "{message}" to targetBuddy
|
||||
end tell"""
|
||||
|
||||
osascript_process = Popen(
|
||||
['osascript', '-e', APPLESCRIPT], stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = osascript_process.communicate()
|
||||
|
||||
|
||||
def instance():
|
||||
import sys
|
||||
if whereis_exe('osascript'):
|
||||
return MacOSSMS()
|
||||
sys.stderr.write('osascript not found.')
|
||||
return SMS()
|
@ -3,7 +3,7 @@ MacOS X Storage Path
|
||||
--------------------
|
||||
'''
|
||||
|
||||
from plyer.facades import StoragePath
|
||||
from sbapp.plyer.facades import StoragePath
|
||||
from pyobjus import autoclass
|
||||
|
||||
NSFileManager = autoclass('NSFileManager')
|
||||
|
@ -1,6 +1,6 @@
|
||||
import subprocess
|
||||
from plyer.facades import TTS
|
||||
from plyer.utils import whereis_exe
|
||||
from sbapp.plyer.facades import TTS
|
||||
from sbapp.plyer.utils import whereis_exe
|
||||
|
||||
|
||||
class NativeSayTextToSpeech(TTS):
|
||||
|
@ -4,8 +4,8 @@ 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
|
||||
from sbapp.plyer.facades import UniqueID
|
||||
from sbapp.plyer.utils import whereis_exe
|
||||
|
||||
|
||||
class OSXUniqueID(UniqueID):
|
||||
|
@ -1,7 +1,7 @@
|
||||
from pyobjus import autoclass
|
||||
from pyobjus.dylib_manager import load_framework, INCLUDE
|
||||
|
||||
from plyer.facades import Wifi
|
||||
from sbapp.plyer.facades import Wifi
|
||||
|
||||
load_framework(INCLUDE.Foundation)
|
||||
load_framework(INCLUDE.CoreWLAN)
|
||||
|
@ -14,8 +14,8 @@ from ctypes import (
|
||||
)
|
||||
from ctypes.wintypes import DWORD, UINT
|
||||
|
||||
from plyer.facades import Audio
|
||||
from plyer.platforms.win.storagepath import WinStoragePath
|
||||
from sbapp.plyer.facades import Audio
|
||||
from sbapp.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
|
||||
|
@ -2,8 +2,8 @@
|
||||
Module of Windows API for plyer.battery.
|
||||
'''
|
||||
|
||||
from plyer.platforms.win.libs.batterystatus import battery_status
|
||||
from plyer.facades import Battery
|
||||
from sbapp.plyer.platforms.win.libs.batterystatus import battery_status
|
||||
from sbapp.plyer.facades import Battery
|
||||
from ctypes.wintypes import BYTE
|
||||
|
||||
|
||||
|
@ -11,7 +11,7 @@ from ctypes.wintypes import (
|
||||
BYTE, DWORD, WORD
|
||||
)
|
||||
|
||||
from plyer.facades import CPU
|
||||
from sbapp.plyer.facades import CPU
|
||||
|
||||
|
||||
KERNEL = windll.kernel32
|
||||
|
@ -3,7 +3,7 @@ Module of Win API for plyer.devicename.
|
||||
'''
|
||||
|
||||
import socket
|
||||
from plyer.facades import DeviceName
|
||||
from sbapp.plyer.facades import DeviceName
|
||||
|
||||
|
||||
class WinDeviceName(DeviceName):
|
||||
|
@ -7,7 +7,7 @@ try:
|
||||
from urllib.parse import quote
|
||||
except ImportError:
|
||||
from urllib import quote
|
||||
from plyer.facades import Email
|
||||
from sbapp.plyer.facades import Email
|
||||
|
||||
|
||||
class WindowsEmail(Email):
|
||||
|
@ -3,7 +3,7 @@ Windows file chooser
|
||||
--------------------
|
||||
'''
|
||||
|
||||
from plyer.facades import FileChooser
|
||||
from sbapp.plyer.facades import FileChooser
|
||||
from win32com.shell.shell import (
|
||||
SHBrowseForFolder as browse,
|
||||
SHGetPathFromIDList as get_path
|
||||
@ -84,7 +84,7 @@ class Win32FileChooser:
|
||||
# e.g. open_file(filters=['*.txt', '*.py'])
|
||||
filters = ""
|
||||
for f in self.filters:
|
||||
if type(f) == str:
|
||||
if isinstance(f, str):
|
||||
filters += (f + "\x00") * 2
|
||||
else:
|
||||
filters += f[0] + "\x00" + ";".join(f[1:]) + "\x00"
|
||||
|
@ -3,7 +3,7 @@ try:
|
||||
except Exception:
|
||||
raise NotImplementedError()
|
||||
|
||||
from plyer.facades import Keystore
|
||||
from sbapp.plyer.facades import Keystore
|
||||
|
||||
|
||||
class WinKeystore(Keystore):
|
||||
|
@ -12,7 +12,7 @@ import ctypes
|
||||
import atexit
|
||||
from threading import RLock
|
||||
|
||||
from plyer.platforms.win.libs import win_api_defs
|
||||
from sbapp.plyer.platforms.win.libs import win_api_defs
|
||||
|
||||
|
||||
WS_OVERLAPPED = 0x00000000
|
||||
|
@ -6,7 +6,7 @@ __all__ = ('battery_status')
|
||||
|
||||
|
||||
import ctypes
|
||||
from plyer.platforms.win.libs import win_api_defs
|
||||
from sbapp.plyer.platforms.win.libs import win_api_defs
|
||||
|
||||
|
||||
def battery_status():
|
||||
|
@ -4,8 +4,8 @@ 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
|
||||
from sbapp.plyer.facades import Notification
|
||||
from sbapp.plyer.platforms.win.libs.balloontip import balloon_tip
|
||||
|
||||
|
||||
class WindowsNotification(Notification):
|
||||
|
@ -18,8 +18,8 @@ from win32con import (
|
||||
SRCCOPY
|
||||
)
|
||||
|
||||
from plyer.facades import Screenshot
|
||||
from plyer.platforms.win.storagepath import WinStoragePath
|
||||
from sbapp.plyer.facades import Screenshot
|
||||
from sbapp.plyer.platforms.win.storagepath import WinStoragePath
|
||||
|
||||
|
||||
class WinScreenshot(Screenshot):
|
||||
|
@ -3,9 +3,9 @@ Windows Storage Path
|
||||
--------------------
|
||||
'''
|
||||
|
||||
from plyer.facades import StoragePath
|
||||
from sbapp.plyer.facades import StoragePath
|
||||
from os.path import expanduser
|
||||
from plyer.platforms.win.libs.win_api_defs import get_PATH
|
||||
from sbapp.plyer.platforms.win.libs.win_api_defs import get_PATH
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import subprocess
|
||||
from plyer.facades import TTS
|
||||
from plyer.utils import whereis_exe
|
||||
from sbapp.plyer.facades import TTS
|
||||
from sbapp.plyer.utils import whereis_exe
|
||||
|
||||
|
||||
class EspeakTextToSpeech(TTS):
|
||||
|
@ -10,7 +10,7 @@ except ImportError:
|
||||
except ImportError:
|
||||
raise NotImplementedError()
|
||||
|
||||
from plyer.facades import UniqueID
|
||||
from sbapp.plyer.facades import UniqueID
|
||||
|
||||
|
||||
class WinUniqueID(UniqueID):
|
||||
|
@ -1,5 +1,5 @@
|
||||
import plyer.platforms.win.libs.wifi_defs as wifi_lib
|
||||
from plyer.facades import Wifi
|
||||
import sbapp.plyer.platforms.win.libs.wifi_defs as wifi_lib
|
||||
from sbapp.plyer.facades import Wifi
|
||||
|
||||
|
||||
class WindowWifi(Wifi):
|
||||
|
@ -1,76 +0,0 @@
|
||||
'''
|
||||
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
|
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB |
@ -1,110 +0,0 @@
|
||||
'''
|
||||
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()
|
@ -1,413 +0,0 @@
|
||||
'''
|
||||
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 <min_version, max_version>.
|
||||
|
||||
.. 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 <class AppleSmartBattery,\
|
||||
id 0x1000002c9, registered, matched, active, busy 0 (0 ms), retain 6>
|
||||
{{
|
||||
"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()
|
@ -1,144 +0,0 @@
|
||||
'''
|
||||
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()
|
@ -1,262 +0,0 @@
|
||||
'''
|
||||
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 <min_version, max_version>.
|
||||
|
||||
.. 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()
|
@ -1,77 +0,0 @@
|
||||
'''
|
||||
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()
|
@ -1,48 +0,0 @@
|
||||
'''
|
||||
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()
|
@ -1,183 +0,0 @@
|
||||
'''
|
||||
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
|
||||
# <class '<mod>.<platform><cls>'> e.g.:
|
||||
# <class 'plyer.platforms.win.dummy . WinDummy'>
|
||||
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()
|
@ -1,209 +0,0 @@
|
||||
'''
|
||||
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()
|
@ -1,137 +0,0 @@
|
||||
'''
|
||||
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()
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user