More smart device plumbing

This commit is contained in:
Kovid Goyal 2012-07-25 13:15:43 +05:30
commit 859d8c6f50
9 changed files with 390 additions and 10 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -514,6 +514,67 @@ class DevicePlugin(Plugin):
''' '''
pass pass
# Dynamic control interface
# All of these methods are called on the device_manager thread
def is_dynamically_controllable(self):
'''
Called by the device manager when starting plugins. If this method returns
a string, then a) it supports the device manager's dynamic control
interface, and b) that name is to be used when talking to the plugin.
This method must be called from the device_manager thread.
'''
return None
def start_plugin(self):
'''
This method is called to start the plugin. The plugin should begin
to accept device connections however it does that. If the plugin is
already accepting connections, then do nothing.
This method must be called from the device_manager thread.
'''
pass
def stop_plugin(self):
'''
This method is called to stop the plugin. The plugin should no longer
accept connections, and should cleanup behind itself. It is likely that
this method should call shutdown. If the plugin is already not accepting
connections, then do nothing.
This method must be called from the device_manager thread.
'''
pass
def get_option(self, opt_string, default=None):
'''
Return the value of the option indicated by opt_string. This method can
be called when the plugin is not started. Return None if the option does
not exist.
This method must be called from the device_manager thread.
'''
return default
def set_option(self, opt_string, opt_value):
'''
Set the value of the option indicated by opt_string. This method can
be called when the plugin is not started.
This method must be called from the device_manager thread.
'''
pass
def is_running(self):
'''
Return True if the plugin is started, otherwise false
This method must be called from the device_manager thread.
'''
return False
class BookList(list): class BookList(list):
''' '''
A list of books. Each Book object must have the fields A list of books. Each Book object must have the fields

View File

@ -14,6 +14,7 @@ from calibre.utils.smtp import config as email_config
from calibre.constants import iswindows, isosx from calibre.constants import iswindows, isosx
from calibre.customize.ui import is_disabled from calibre.customize.ui import is_disabled
from calibre.devices.bambook.driver import BAMBOOK from calibre.devices.bambook.driver import BAMBOOK
from calibre.gui2.dialogs.smartdevice import SmartdeviceDialog
from calibre.gui2 import info_dialog from calibre.gui2 import info_dialog
class ShareConnMenu(QMenu): # {{{ class ShareConnMenu(QMenu): # {{{
@ -24,6 +25,7 @@ class ShareConnMenu(QMenu): # {{{
config_email = pyqtSignal() config_email = pyqtSignal()
toggle_server = pyqtSignal() toggle_server = pyqtSignal()
control_smartdevice = pyqtSignal()
dont_add_to = frozenset(['context-menu-device']) dont_add_to = frozenset(['context-menu-device'])
def __init__(self, parent=None): def __init__(self, parent=None):
@ -56,6 +58,11 @@ class ShareConnMenu(QMenu): # {{{
_('Start Content Server')) _('Start Content Server'))
self.toggle_server_action.triggered.connect(lambda x: self.toggle_server_action.triggered.connect(lambda x:
self.toggle_server.emit()) self.toggle_server.emit())
self.control_smartdevice_action = \
self.addAction(QIcon(I('dot_green.png')),
_('Control Smart Device Connections'))
self.control_smartdevice_action.triggered.connect(lambda x:
self.control_smartdevice.emit())
self.addSeparator() self.addSeparator()
self.email_actions = [] self.email_actions = []
@ -80,6 +87,9 @@ class ShareConnMenu(QMenu): # {{{
text = _('Stop Content Server') + ' [%s]'%get_external_ip() text = _('Stop Content Server') + ' [%s]'%get_external_ip()
self.toggle_server_action.setText(text) self.toggle_server_action.setText(text)
def hide_smartdevice_menus(self):
self.control_smartdevice_action.setVisible(False)
def build_email_entries(self, sync_menu): def build_email_entries(self, sync_menu):
from calibre.gui2.device import DeviceAction from calibre.gui2.device import DeviceAction
for ac in self.email_actions: for ac in self.email_actions:
@ -158,6 +168,7 @@ class ConnectShareAction(InterfaceAction):
def genesis(self): def genesis(self):
self.share_conn_menu = ShareConnMenu(self.gui) self.share_conn_menu = ShareConnMenu(self.gui)
self.share_conn_menu.toggle_server.connect(self.toggle_content_server) self.share_conn_menu.toggle_server.connect(self.toggle_content_server)
self.share_conn_menu.control_smartdevice.connect(self.control_smartdevice)
self.share_conn_menu.config_email.connect(partial( self.share_conn_menu.config_email.connect(partial(
self.gui.iactions['Preferences'].do_config, self.gui.iactions['Preferences'].do_config,
initial_plugin=('Sharing', 'Email'))) initial_plugin=('Sharing', 'Email')))
@ -200,8 +211,21 @@ class ConnectShareAction(InterfaceAction):
if not self.stopping_msg.isVisible(): if not self.stopping_msg.isVisible():
self.stopping_msg.exec_() self.stopping_msg.exec_()
return return
self.gui.content_server = None self.gui.content_server = None
self.stopping_msg.accept() self.stopping_msg.accept()
def control_smartdevice(self):
sd_dialog = SmartdeviceDialog(self.gui)
sd_dialog.exec_()
self.set_smartdevice_icon()
def check_smartdevice_menus(self):
if not self.gui.device_manager.is_enabled('smartdevice'):
self.share_conn_menu.hide_smartdevice_menus()
def set_smartdevice_icon(self):
running = self.gui.device_manager.is_running('smartdevice')
if running:
self.share_conn_menu.control_smartdevice_action.setIcon(QIcon(I('dot_green.png')))
else:
self.share_conn_menu.control_smartdevice_action.setIcon(QIcon(I('dot_red.png')))

View File

@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
# Imports {{{ # Imports {{{
import os, traceback, Queue, time, cStringIO, re, sys import os, traceback, Queue, time, cStringIO, re, sys
from threading import Thread from threading import Thread, Event
from PyQt4.Qt import (QMenu, QAction, QActionGroup, QIcon, SIGNAL, from PyQt4.Qt import (QMenu, QAction, QActionGroup, QIcon, SIGNAL,
Qt, pyqtSignal, QDialog, QObject, QVBoxLayout, Qt, pyqtSignal, QDialog, QObject, QVBoxLayout,
@ -30,6 +30,7 @@ from calibre.constants import DEBUG
from calibre.utils.config import prefs, tweaks from calibre.utils.config import prefs, tweaks
from calibre.utils.magick.draw import thumbnail from calibre.utils.magick.draw import thumbnail
from calibre.library.save_to_disk import find_plugboard from calibre.library.save_to_disk import find_plugboard
from calibre.gui2 import is_gui_thread
# }}} # }}}
class DeviceJob(BaseJob): # {{{ class DeviceJob(BaseJob): # {{{
@ -145,6 +146,10 @@ class DeviceManager(Thread): # {{{
self._device_information = None self._device_information = None
self.current_library_uuid = None self.current_library_uuid = None
self.call_shutdown_on_disconnect = False self.call_shutdown_on_disconnect = False
self.devices_initialized = Event()
self.dynamic_plugins = {}
self.dynamic_plugin_requests = Queue.Queue(0)
self.dynamic_plugin_responses = Queue.Queue(0)
def report_progress(self, *args): def report_progress(self, *args):
pass pass
@ -286,6 +291,10 @@ class DeviceManager(Thread): # {{{
# Do any device-specific startup processing. # Do any device-specific startup processing.
for d in self.devices: for d in self.devices:
self.run_startup(d) self.run_startup(d)
n = d.is_dynamically_controllable()
if n:
self.dynamic_plugins[n] = d
self.devices_initialized.set()
while self.keep_going: while self.keep_going:
kls = None kls = None
@ -309,6 +318,7 @@ class DeviceManager(Thread): # {{{
traceback.print_exc() traceback.print_exc()
else: else:
self.detect_device() self.detect_device()
while True: while True:
job = self.next() job = self.next()
if job is not None: if job is not None:
@ -319,8 +329,15 @@ class DeviceManager(Thread): # {{{
self.current_job = None self.current_job = None
else: else:
break break
time.sleep(self.sleep_time) while True:
dynamic_method = None
try:
(dynamic_method, args, kwargs) = \
self.dynamic_plugin_requests.get(timeout=self.sleep_time)
res = dynamic_method(*args, **kwargs)
self.dynamic_plugin_responses.put(res)
except Queue.Empty:
break
# We are exiting. Call the shutdown method for each plugin # We are exiting. Call the shutdown method for each plugin
for p in self.devices: for p in self.devices:
try: try:
@ -508,6 +525,51 @@ class DeviceManager(Thread): # {{{
if self.connected_device: if self.connected_device:
self.connected_device.set_driveinfo_name(location_code, name) self.connected_device.set_driveinfo_name(location_code, name)
# dynamic plugin interface
# This is a helper function that handles queueing with the device manager
def _queue_request(self, name, method, *args, **kwargs):
if not is_gui_thread():
raise ValueError(
'The device_manager dynamic plugin methods must be called from the GUI thread')
try:
d = self.dynamic_plugins.get(name, None)
if d:
self.dynamic_plugin_requests.put((getattr(d, method), args, kwargs))
return self.dynamic_plugin_responses.get()
except:
traceback.print_exc()
return kwargs.get('default', None)
# The dynamic plugin methods below must be called on the GUI thread. They
# will switch to the device thread before calling the plugin.
def start_plugin(self, name):
self._queue_request(name, 'start_plugin')
def stop_plugin(self, name):
self._queue_request(name, 'stop_plugin')
def get_option(self, name, opt_string, default=None):
return self._queue_request(name, 'get_option', opt_string, default=default)
def set_option(self, name, opt_string, opt_value):
self._queue_request(name, 'set_option', opt_string, opt_value)
def is_running(self, name):
if self._queue_request(name, 'is_running'):
return True
return False
def is_enabled(self, name):
try:
d = self.dynamic_plugins.get(name, None)
if d:
return True
except:
pass
return False
# }}} # }}}
class DeviceAction(QAction): # {{{ class DeviceAction(QAction): # {{{
@ -708,6 +770,7 @@ class DeviceMixin(object): # {{{
self.job_manager, Dispatcher(self.status_bar.show_message), self.job_manager, Dispatcher(self.status_bar.show_message),
Dispatcher(self.show_open_feedback)) Dispatcher(self.show_open_feedback))
self.device_manager.start() self.device_manager.start()
self.device_manager.devices_initialized.wait()
if tweaks['auto_connect_to_folder']: if tweaks['auto_connect_to_folder']:
self.connect_to_folder_named(tweaks['auto_connect_to_folder']) self.connect_to_folder_named(tweaks['auto_connect_to_folder'])

View File

@ -0,0 +1,81 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import re
from PyQt4.QtGui import QDialog, QLineEdit
from PyQt4.QtCore import SIGNAL, Qt
from calibre.gui2.dialogs.smartdevice_ui import Ui_Dialog
from calibre.gui2 import dynamic
class SmartdeviceDialog(QDialog, Ui_Dialog):
def __init__(self, parent):
QDialog.__init__(self, parent)
Ui_Dialog.__init__(self)
self.setupUi(self)
self.msg.setText(
_('This dialog starts and stops the smart device app interface. '
'When you start the interface, you might see some messages from '
'your computer\'s firewall or anti-virus manager asking you '
'if it is OK for calibre to connect to the network. <B>Please '
'answer yes</b>. If you do not, the app will not work. It will '
'be unable to connect to calibre.'))
self.password_box.setToolTip('<p>' +
_('Use a password if calibre is running on a network that '
'is not secure. For example, if you run calibre on a laptop, '
'use that laptop in an airport, and want to connect your '
'smart device to calibre, you should use a password.') + '</p>')
self.run_box.setToolTip('<p>' +
_('Check this box to allow calibre to accept connections from the '
'smart device. Uncheck the box to prevent connections.') + '</p>')
self.autostart_box.setToolTip('<p>' +
_('Check this box if you want calibre to automatically start the '
'smart device interface when calibre starts. You should not do '
'this if you are using a network that is not secure and you '
'are not setting a password.') + '</p>')
self.connect(self.show_password, SIGNAL('stateChanged(int)'), self.toggle_password)
self.autostart_box.stateChanged.connect(self.autostart_changed)
self.device_manager = parent.device_manager
if self.device_manager.is_running('smartdevice'):
self.run_box.setChecked(True)
else:
self.run_box.setChecked(False)
if self.device_manager.get_option('smartdevice', 'autostart'):
self.autostart_box.setChecked(True)
self.run_box.setChecked(True)
self.run_box.setEnabled(False)
pw = self.device_manager.get_option('smartdevice', 'password')
if pw:
self.password_box.setText(pw)
def autostart_changed(self):
if self.autostart_box.isChecked():
self.run_box.setChecked(True)
self.run_box.setEnabled(False)
else:
self.run_box.setEnabled(True)
def toggle_password(self, state):
if state == Qt.Unchecked:
self.password_box.setEchoMode(QLineEdit.Password)
else:
self.password_box.setEchoMode(QLineEdit.Normal)
def accept(self):
self.device_manager.set_option('smartdevice', 'password',
unicode(self.password_box.text()))
self.device_manager.set_option('smartdevice', 'autostart',
self.autostart_box.isChecked())
if self.run_box.isChecked():
self.device_manager.start_plugin('smartdevice')
else:
self.device_manager.stop_plugin('smartdevice')
QDialog.accept(self)

View File

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>209</height>
</rect>
</property>
<property name="windowTitle">
<string>Smart device control</string>
</property>
<property name="windowIcon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/mimetypes/unknown.png</normaloff>:/images/mimetypes/unknown.png</iconset>
</property>
<layout class="QGridLayout">
<item row="4" column="1">
<widget class="QCheckBox" name="autostart_box">
<property name="text">
<string>&amp;Automatically allow connections at startup</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLabel" name="label_43">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>100</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="password_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>100</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="placeholderText">
<string>Optional password for security</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="run_box">
<property name="text">
<string>&amp;Allow connections</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="msg">
<property name="text">
<string>TextLabel</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Password:</string>
</property>
<property name="buddy">
<cstring>password_box</cstring>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QCheckBox" name="show_password">
<property name="text">
<string>&amp;Show password</string>
</property>
</widget>
</item>
<item row="6" column="0" colspan="3">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -337,6 +337,15 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
if config['autolaunch_server']: if config['autolaunch_server']:
self.start_content_server() self.start_content_server()
smartdevice_actions = self.iactions['Connect Share']
smartdevice_actions.check_smartdevice_menus()
if self.device_manager.get_option('smartdevice', 'autostart'):
try:
self.device_manager.start_plugin('smartdevice')
except:
pass
smartdevice_actions.set_smartdevice_icon()
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection) self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
self.read_settings() self.read_settings()

View File

@ -955,11 +955,16 @@ class Reaper(threading.Thread):
return return
if globals()['_GLOBAL_DONE']: if globals()['_GLOBAL_DONE']:
return return
now = currentTimeMillis() try:
for record in self.zeroconf.cache.entries(): # can get here in a race condition with shutdown. Swallow the
if record.isExpired(now): # exception and run around the loop again.
self.zeroconf.updateRecord(now, record) now = currentTimeMillis()
self.zeroconf.cache.remove(record) for record in self.zeroconf.cache.entries():
if record.isExpired(now):
self.zeroconf.updateRecord(now, record)
self.zeroconf.cache.remove(record)
except:
pass
class ServiceBrowser(threading.Thread): class ServiceBrowser(threading.Thread):