/// <summary> /// Attempt to send the book to the device /// </summary> public void SendBookAsync(Book.Book book, Color backColor, AndroidPublishSettings settings = null) { try { var backgroundWorker = new BackgroundWorker(); backgroundWorker.DoWork += (sender, args) => { SendBookDoWork(book, backColor, settings); }; backgroundWorker.RunWorkerCompleted += (sender, args) => { if (args.Error != null) { FailSendBook(args.Error); } else { Stopped(); } }; backgroundWorker.RunWorkerAsync(); } catch (Exception e) { FailSendBook(e); } }
private void HandleOneReadyDeviceFound(Book.Book book, Color backColor, AndroidPublishSettings settings = null) { _progress.MessageWithParams(idSuffix: "Connected", message: "Connected to {0} via USB...", comment: "{0} is a the name of the device Bloom connected to", progressKind: ProgressKind.Progress, parameters: _androidDeviceUsbConnection.GetDeviceName()); SendBookAsync(book, backColor, settings); }
/// <summary> /// Attempt to establish a connection with an Android device; then send it the book. /// </summary> public void ConnectAndSendToOneDevice(Book.Book book, Color backColor, AndroidPublishSettings settings = null) { _stopLookingForDevice = false; _device = null; //The UX here is to only allow one device plugged in a time. while (!_stopLookingForDevice && _device == null) { var devices = EnumerateAllDevices(); if (!ConnectAndSendToOneDeviceInternal(devices, book, backColor, settings)) { Thread.Sleep(1000); } } }
/// <summary> /// Attempt to connect to a device /// </summary> /// <param name="book"></param> public void Connect(Book.Book book, Color backColor, AndroidPublishSettings settings = null) { try { // Calls to this come from JavaScript, not sure they will always be on the UI thread. // Before I added this, I definitely saw race conditions with more than one thread trying // to figure out what was connected. lock (this) { PublishToAndroidApi.CheckBookLayout(book, _progress); if (_connectionHandler != null) { // we're in an odd state...should only be able to click the button that calls this // while stopped. // Try to really get into the right state in case the user tries again. _androidDeviceUsbConnection.StopFindingDevice(); return; } // Create this while locked...once we have it, can't enter the main logic of this method // on another thread. _connectionHandler = new BackgroundWorker(); } _progress.Message(idSuffix: "LookingForDevice", comment: "This is a progress message; MTP is an acronym for the system that allows computers to access files on devices.", message: "Looking for an Android device connected by USB cable and set up for MTP..."); _androidDeviceUsbConnection.OneReadyDeviceFound = HandleFoundAReadyDevice; _androidDeviceUsbConnection.OneReadyDeviceNotFound = HandleFoundOneNonReadyDevice; // Don't suppress the first message after (re)starting. _previousDeviceNotFoundReportType = DeviceNotFoundReportType.Unknown; _connectionHandler.DoWork += (sender, args) => _androidDeviceUsbConnection.ConnectAndSendToOneDevice(book, backColor, settings); _connectionHandler.RunWorkerCompleted += (sender, args) => { if (args.Error != null) { UsbFailConnect(args.Error); } _connectionHandler = null; // now OK to try to connect again. }; _connectionHandler.RunWorkerAsync(); } catch (Exception e) { UsbFailConnect(e); } }
protected virtual void SendBookDoWork(Book.Book book, Color backColor, AndroidPublishSettings settings = null) { PublishToAndroidApi.SendBook(book, _bookServer, null, (publishedFileName, path) => { _lastPublishedBloomdSize = GetSizeOfBloomdFile(path); _androidDeviceUsbConnection.SendBook(path); }, _progress, (publishedFileName, bookTitle) => _androidDeviceUsbConnection.BookExists(publishedFileName) ? _progress.GetTitleMessage("ReplacingBook", "Replacing existing \"{0}\"...", bookTitle) : _progress.GetTitleMessage("SendingBook", "Sending \"{0}\" to your Android device...", bookTitle), publishedFileName => _androidDeviceUsbConnection.BookExists(publishedFileName), backColor, settings: settings); PublishToAndroidApi.ReportAnalytics("usb", book); }
public void Start(Book.Book book, CollectionSettings collectionSettings, Color backColor, AndroidPublishSettings publishSettings = null) { if (_wifiAdvertiser != null) { Stop(); } // This listens for a BloomReader to request a book. // It requires a firewall hole allowing Bloom to receive messages on _portToListen. // We initialize it before starting the Advertiser to avoid any chance of a race condition // where a BloomReader manages to request an advertised book before we start the listener. _wifiListener = new BloomReaderUDPListener(); _wifiListener.NewMessageReceived += (sender, args) => { var json = Encoding.UTF8.GetString(args.Data); try { dynamic settings = JsonConvert.DeserializeObject(json); // The property names used here must match the ones in BloomReader, doInBackground method of SendMessage, // a private class of NewBookListenerService. var androidIpAddress = (string)settings.deviceAddress; var androidName = (string)settings.deviceName; // This prevents the device (or other devices) from queuing up requests while we're busy with this one. // In effect, the Android is only allowed to request a retry after we've given up this try at sending. // Of course, there are async effects from network latency. But if we do get another request while // handling this one, we will ignore it, since StartSendBook checks for a transfer in progress. _wifiAdvertiser.Paused = true; StartSendBookOverWiFi(book, androidIpAddress, androidName, backColor, publishSettings); // Returns immediately. But we don't resume advertisements until the async send completes. } // If there's something wrong with the JSON (maybe an obsolete or newer version of reader?) // just ignore the request. catch (Exception ex) when(ex is JsonReaderException || ex is JsonSerializationException) { _progress.Message(idSuffix: "BadBookRequest", message: "Got a book request we could not process. Possibly the device is running an incompatible version of BloomReader?", progressKind: ProgressKind.Error); //this is too technical/hard to translate _progress.MessageWithoutLocalizing($" Request contains {json}; trying to interpret as JSON we got {ex.Message}", kind: ProgressKind.Error); } }; var pathHtmlFile = book.GetPathHtmlFile(); _wifiAdvertiser = new WiFiAdvertiser(_progress) { BookTitle = BookStorage.SanitizeNameForFileSystem(book.Title), // must be the exact same name as the file we will send if requested TitleLanguage = book.BookData.Language1.Iso639Code, BookVersion = Book.Book.MakeVersionCode(File.ReadAllText(pathHtmlFile), pathHtmlFile) }; PublishToAndroidApi.CheckBookLayout(book, _progress); _wifiAdvertiser.Start(); var part1 = LocalizationManager.GetDynamicString(appId: "Bloom", id: "PublishTab.Android.Wifi.Progress.WifiInstructions1", englishText: "On the Android, run Bloom Reader, open the menu and choose 'Receive Books via WiFi'."); var part2 = LocalizationManager.GetDynamicString(appId: "Bloom", id: "PublishTab.Android.Wifi.Progress.WifiInstructions2", englishText: "You can do this on as many devices as you like. Make sure each device is connected to the same network as this computer."); // can only have one instruction up at a time, so we concatenate these _progress.MessageWithoutLocalizing(part1 + " " + part2, ProgressKind.Instruction); }
private void StartSendBookOverWiFi(Book.Book book, string androidIpAddress, string androidName, Color backColor, AndroidPublishSettings settings = null) { try { StartSendBookToClientOnLocalSubNet(book, androidIpAddress, androidName, backColor, settings); } catch (Exception e) { ReportException(e); } }
/// <summary> /// Send the book to a client over local network, typically WiFi (at least on Android end). /// This is currently called on the UDPListener thread. /// Enhance: if we spin off another thread to do the transfer, especially if we create the file /// and read it into memory once and share the content, we can probably serve multiple /// requesting devices much faster. Currently, we are only handling one request at a time, /// since we pause advertising while sending and ignore requests that come in during sending. /// If the user switches away from the Android tab while a transfer /// is in progress, the thread will continue and complete the request. Quitting Bloom /// is likely to leave the transfer incomplete. /// </summary> private void StartSendBookToClientOnLocalSubNet(Book.Book book, string androidIpAddress, string androidName, Color backColor, AndroidPublishSettings settings = null) { // Locked in case more than one thread at a time can handle incoming packets, though I don't think // this is true. Also, Stop() on the main thread cares whether _wifiSender is null. lock (this) { // We only support one send at a time. If we somehow get more than one request, we ignore the other. // The device will retry soon if still listening and we are still advertising. if (_wifiSender != null) // indicates transfer in progress { return; } // now THIS transfer is 'in progress' as far as any thread checking this is concerned. _wifiSender = new WebClient(); } _wifiSender.UploadDataCompleted += WifiSenderUploadCompleted; // Now we actually start the send...but using an async API, so there's no long delay here. PublishToAndroidApi.SendBook(book, _bookServer, null, (publishedFileName, bloomDPath) => { var androidHttpAddress = "http://" + androidIpAddress + ":5914"; // must match BloomReader SyncServer._serverPort. _wifiSender.UploadDataAsync(new Uri(androidHttpAddress + "/putfile?path=" + Uri.EscapeDataString(publishedFileName)), File.ReadAllBytes(bloomDPath)); Debug.WriteLine($"upload started to http://{androidIpAddress}:5914 ({androidName}) for {publishedFileName}"); }, _progress, (publishedFileName, bookTitle) => _progress.GetMessageWithParams(idSuffix: "Sending", comment: "{0} is the name of the book, {1} is the name of the device", message: "Sending \"{0}\" to device {1}", parameters: new object[] { bookTitle, androidName }), null, backColor, settings: settings); // Occasionally preparing a book for sending will, despite our best efforts, result in a different sha. // For example, it might change missing or out-of-date mp3 files. In case the sha we just computed // is different from the one we're advertising, update the advertisement, so at least subsequent // advertisements will conform to the version the device just got. _wifiAdvertiser.BookVersion = BookCompressor.LastVersionCode; lock (this) { // The UploadDataCompleted event handler quit working at Bloom 4.6.1238 Alpha (Windows test build). // The data upload still works, but the event handler is *NEVER* called. Trying to revise the upload // by using UploadDataTaskAsync with async/await did not work any better: the await never happened. // To get around this bug, we introduce a timer that periodically checks the IsBusy flag of the // _wifiSender object. It's a hack, but I haven't come up with anything better in two days of // looking at this problem. // See https://issues.bloomlibrary.org/youtrack/issue/BL-7227 for details. if (_uploadTimer == null) { _uploadTimer = new System.Timers.Timer { Interval = 500.0, Enabled = false }; _uploadTimer.Elapsed += (sender, args) => { if (_wifiSender != null && _wifiSender.IsBusy) { return; } _uploadTimer.Stop(); Debug.WriteLine("upload timed out, appears to be finished"); WifiSenderUploadCompleted(_uploadTimer, null); }; } _uploadTimer.Start(); } PublishToAndroidApi.ReportAnalytics("wifi", book); }
public static void Save(Book.Book book, BookServer bookServer, Color backColor, WebSocketProgress progress, AndroidPublishSettings settings = null) { var progressWithL10N = progress.WithL10NPrefix("PublishTab.Android.File.Progress."); var folder = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); if (!string.IsNullOrWhiteSpace(Settings.Default.BloomDeviceFileExportFolder) && Directory.Exists(Settings.Default.BloomDeviceFileExportFolder)) { folder = Settings.Default.BloomDeviceFileExportFolder; } var initialPath = Path.Combine(folder, book.Storage.FolderName + BookCompressor.BloomPubExtensionWithDot); var bloomdFileDescription = LocalizationManager.GetString("PublishTab.Android.bloomdFileFormatLabel", "Bloom Book for Devices", "This is shown in the 'Save' dialog when you save a bloom book in the format that works with the Bloom Reader Android App"); var filter = $"{bloomdFileDescription}|*{BookCompressor.BloomPubExtensionWithDot}"; var destFileName = Utils.MiscUtils.GetOutputFilePathOutsideCollectionFolder(initialPath, filter); if (String.IsNullOrEmpty(destFileName)) { return; } Settings.Default.BloomDeviceFileExportFolder = Path.GetDirectoryName(destFileName); PublishToAndroidApi.CheckBookLayout(book, progress); PublishToAndroidApi.SendBook(book, bookServer, destFileName, null, progressWithL10N, (publishedFileName, bookTitle) => progressWithL10N.GetMessageWithParams("Saving", "{0} is a file path", "Saving as {0}", destFileName), null, backColor, settings: settings); PublishToAndroidApi.ReportAnalytics("file", book); }
public static void Save(Book.Book book, BookServer bookServer, Color backColor, WebSocketProgress progress, AndroidPublishSettings settings = null) { var progressWithL10N = progress.WithL10NPrefix("PublishTab.Android.File.Progress."); using (var dlg = new DialogAdapters.SaveFileDialogAdapter()) { dlg.DefaultExt = BookCompressor.ExtensionForDeviceBloomBook; var bloomdFileDescription = LocalizationManager.GetString("PublishTab.Android.bloomdFileFormatLabel", "Bloom Book for Devices", "This is shown in the 'Save' dialog when you save a bloom book in the format that works with the Bloom Reader Android App"); dlg.Filter = $"{bloomdFileDescription}|*{BookCompressor.ExtensionForDeviceBloomBook}"; dlg.FileName = Path.GetFileName(book.FolderPath) + BookCompressor.ExtensionForDeviceBloomBook; if (!string.IsNullOrWhiteSpace(Settings.Default.BloomDeviceFileExportFolder) && Directory.Exists(Settings.Default.BloomDeviceFileExportFolder)) { dlg.InitialDirectory = Settings.Default.BloomDeviceFileExportFolder; //(otherwise leave to default save location) } dlg.OverwritePrompt = true; if (DialogResult.OK == dlg.ShowDialog()) { Settings.Default.BloomDeviceFileExportFolder = Path.GetDirectoryName(dlg.FileName); PublishToAndroidApi.CheckBookLayout(book, progress); PublishToAndroidApi.SendBook(book, bookServer, dlg.FileName, null, progressWithL10N, (publishedFileName, bookTitle) => progressWithL10N.GetMessageWithParams("Saving", "{0} is a file path", "Saving as {0}", dlg.FileName), null, backColor, settings: settings); PublishToAndroidApi.ReportAnalytics("file", book); } } }
/// <summary> /// Creates the .bloompub and bloomdigital folders /// </summary> private static CreateArtifactsExitCode CreateBloomDigitalArtifacts(string bookPath, string creator, string zippedBloomPubOutputPath, string unzippedBloomDigitalOutputPath) { #if DEBUG // Useful for allowing debugging of Bloom while running the harvester //MessageBox.Show("Attach debugger now"); #endif var exitCode = CreateArtifactsExitCode.Success; using (var tempBloomPub = TempFile.CreateAndGetPathButDontMakeTheFile()) { if (String.IsNullOrEmpty(zippedBloomPubOutputPath)) { zippedBloomPubOutputPath = tempBloomPub.Path; } BookServer bookServer = _projectContext.BookServer; var metadata = BookMetaData.FromFolder(bookPath); bool isTemplateBook = metadata.IsSuitableForMakingShells; // Build artifacts the same way from the harvester as on the user's local machine. // (similarly to a bulk publish operation) // See https://issues.bloomlibrary.org/youtrack/issue/BL-10300. var bookInfo = new BookInfo(bookPath, false); var settings = AndroidPublishSettings.GetPublishSettingsForBook(bookServer, bookInfo); using (var folderForUnzipped = new TemporaryFolder("BloomCreateArtifacts_Unzipped")) { // Ensure directory exists, just in case. Directory.CreateDirectory(Path.GetDirectoryName(zippedBloomPubOutputPath)); // Make the bloompub string unzippedPath = BloomPubMaker.CreateBloomPub( zippedBloomPubOutputPath, bookPath, bookServer, new Bloom.web.NullWebSocketProgress(), folderForUnzipped, creator, isTemplateBook, settings); // Currently the zipping process does some things we actually need, like making the cover picture // transparent (BL-7437). Eventually we plan to separate the preparation and zipping steps (BL-7445). // Until that is done, the most reliable way to get an unzipped BloomPUB for our preview is to actually // unzip the BloomPUB. if (!String.IsNullOrEmpty(unzippedBloomDigitalOutputPath)) { SIL.IO.RobustIO.DeleteDirectory(unzippedBloomDigitalOutputPath, recursive: true); // In case the folder isn't already empty // Ensure directory exists, just in case. Directory.CreateDirectory(Path.GetDirectoryName(unzippedBloomDigitalOutputPath)); ZipFile.ExtractToDirectory(zippedBloomPubOutputPath, unzippedBloomDigitalOutputPath); exitCode |= RenameBloomDigitalFiles(unzippedBloomDigitalOutputPath); } } } return(exitCode); }
/// <summary> /// Find the one Android device /// (indicated by the presence of the Android directory as a direct child of a root storage object) /// and send to it. /// </summary> /// <returns>true if it found one ready device</returns> private bool ConnectAndSendToOneDeviceInternal(IEnumerable <IDevice> devices, Book.Book book, Color backColor, AndroidPublishSettings settings = null) { List <IDevice> applicableDevices = new List <IDevice>(); foreach (var device in devices) { var androidFolderPath = GetAndroidFolderPath(device); if (androidFolderPath != null) { applicableDevices.Add(device); _bloomFolderPath = Path.GetDirectoryName(androidFolderPath) + "\\" + kBloomFolderOnDevice; _device = device; } } if (applicableDevices.Count == 1) { try { _device.CreateFolderObjectFromPath(_bloomFolderPath); } catch (Exception e) { SIL.Reporting.Logger.WriteError("Unable to create Bloom folder on device.", e); // Treat it as a no-device situation. _bloomFolderPath = null; _device = null; OneReadyDeviceNotFound?.Invoke(DeviceNotFoundReportType.NoDeviceFound, new List <string>(0)); return(false); } OneReadyDeviceFound(book, backColor, settings); return(true); } _bloomFolderPath = null; _device = null; DeviceNotFoundReportType deviceNotFoundReportType = applicableDevices.Count > 1 ? DeviceNotFoundReportType.MoreThanOneReadyDevice : DeviceNotFoundReportType.NoDeviceFound; OneReadyDeviceNotFound?.Invoke(deviceNotFoundReportType, devices.Select(d => d.Name).ToList()); return(false); }
protected override void SendBookDoWork(Bloom.Book.Book book, Color backColor, AndroidPublishSettings settings) { throw new COMException("MockUsbPublisher threw a fake COMException in SendBookDoWork.", _exceptionToThrow); }
/// <summary> /// Find the one with Bloom Reader installed (indicated by the presence of the Bloom directory /// as a direct child of a root storage object) /// </summary> /// <param name="devices"></param> /// <returns>true if it found a ready device</returns> private bool ConnectAndSendToOneDeviceInternal(IEnumerable <IDevice> devices, Book.Book book, Color backColor, AndroidPublishSettings settings = null) { List <IDevice> applicableDevices = new List <IDevice>(); int totalDevicesFound = 0; foreach (var device in devices) { _bloomFolderPath = GetBloomFolderPath(device); if (_bloomFolderPath != null) { applicableDevices.Add(device); } totalDevicesFound++; } if (applicableDevices.Count == 1) { _device = applicableDevices[0]; // Without this, we're depending on the LAST device we tried being the applicable one. _bloomFolderPath = GetBloomFolderPath(_device); OneReadyDeviceFound(book, backColor, settings); return(true); } _bloomFolderPath = null; if (totalDevicesFound > 0 && applicableDevices.Count == 0) { OneReadyDeviceNotFound(DeviceNotFoundReportType.NoBloomDirectory, devices.Select(d => d.Name).ToList()); return(false); } DeviceNotFoundReportType deviceNotFoundReportType = DeviceNotFoundReportType.NoDeviceFound; if (applicableDevices.Count > 1) { deviceNotFoundReportType = DeviceNotFoundReportType.MoreThanOneReadyDevice; } OneReadyDeviceNotFound?.Invoke(deviceNotFoundReportType, devices.Select(d => d.Name).ToList()); return(false); }