Пример #1
0
        private void UsbFailConnect(Exception e)
        {
            Stop(disposing: false);
            _progress.Message(idSuffix: "UnableToConnect",
                              message: "Unable to connect to any Android device.");

            _progress.MessageWithoutLocalizing("\tTechnical details to share with the development team: " + e, ProgressKind.Error);
            Logger.WriteError(e);
            Stopped();
        }
Пример #2
0
        private void HandleOneReadyDeviceNotFound(DeviceNotFoundReportType reportType, List <string> deviceNames)
        {
            // Don't report the same thing over and over
            if (_previousDeviceNotFoundReportType == reportType)
            {
                return;
            }

            _previousDeviceNotFoundReportType = reportType;

            switch (reportType)
            {
            case DeviceNotFoundReportType.NoDeviceFound:
                _progress.Message("NoDeviceFound", "No device found. Still looking...");
                break;

            case DeviceNotFoundReportType.MoreThanOneReadyDevice:
                _progress.Message(idSuffix: "MoreThanOne",
                                  message: "Please connect only one of the following devices.");
                foreach (var deviceName in deviceNames)
                {
                    _progress.MessageWithoutLocalizing($"\t{deviceName}");
                }
                break;
            }
        }
Пример #3
0
 private void Work()
 {
     _progress.Message(idSuffix: "beginAdvertising", message: "Advertising book to Bloom Readers on local network...");
     try
     {
         while (true)
         {
             if (!Paused)
             {
                 UpdateAdvertisementBasedOnCurrentIpAddress();
                 _client.BeginSend(_sendBytes, _sendBytes.Length, _endPoint, SendCallback, _client);
             }
             Thread.Sleep(1000);
         }
     }
     catch (ThreadAbortException)
     {
         _progress.Message(idSuffix: "Stopped", message: "Stopped Advertising.");
         _client.Close();
     }
     catch (Exception error)
     {
         // not worth localizing
         _progress.MessageWithoutLocalizing($"Error in Advertiser: {error.Message}", ProgressKind.Error);
     }
 }
        public static string StageBloomD(Book.Book book, BookServer bookServer, WebSocketProgress progress, Color backColor, AndroidPublishSettings settings = null)
        {
            progress.Message("PublishTab.Epub.PreparingPreview", "Preparing Preview");                  // message shared with Epub publishing
            if (settings?.LanguagesToInclude != null)
            {
                var message = new LicenseChecker().CheckBook(book, settings.LanguagesToInclude.ToArray());
                if (message != null)
                {
                    progress.MessageWithoutLocalizing(message, MessageKind.Error);
                    return(null);
                }
            }

            _stagingFolder?.Dispose();
            if (AudioProcessor.IsAnyCompressedAudioMissing(book.FolderPath, book.RawDom))
            {
                progress.Message("CompressingAudio", "Compressing audio files");
                AudioProcessor.TryCompressingAudioAsNeeded(book.FolderPath, book.RawDom);
            }
            // We don't use the folder found here, but this method does some checks we want done.
            BookStorage.FindBookHtmlInFolder(book.FolderPath);
            _stagingFolder = new TemporaryFolder(StagingFolder);
            var modifiedBook = BloomReaderFileMaker.PrepareBookForBloomReader(book.FolderPath, bookServer, _stagingFolder, progress, settings: settings);

            progress.Message("Common.Done", "Shown in a list of messages when Bloom has completed a task.", "Done");
            return(modifiedBook.FolderPath.ToLocalhost());
        }
Пример #5
0
 public static void SendBatchedWarningMessagesToProgress(ISet <string> warningMessages, WebSocketProgress progress)
 {
     if (warningMessages.Any())
     {
         progress.Message("Common.Warning", "Warning", ProgressKind.Warning, false);
     }
     foreach (var warningMessage in warningMessages)
     {
         // Messages are already localized
         progress.MessageWithoutLocalizing(warningMessage, ProgressKind.Warning);
     }
 }
Пример #6
0
        private void HandleFoundOneNonReadyDevice(DeviceNotFoundReportType reportType, List <string> deviceNames)
        {
            // Don't report the same thing over and over
            if (_previousDeviceNotFoundReportType == reportType)
            {
                return;
            }

            _previousDeviceNotFoundReportType = reportType;

            switch (reportType)
            {
            case DeviceNotFoundReportType.NoDeviceFound:
                _progress.Message("NoDeviceFound", "No device found. Still looking...");
                break;

            case DeviceNotFoundReportType.NoBloomDirectory:
                // I made this "running" instead of "installed" because I'm assuming
                // we wouldn't get a bloom directory just from installing. We don't actually need it to be
                // running, but this keeps the instructions simple.
                _progress.Message(id: "DeviceWithoutBloomReader",
                                  message: "The following devices are connected but do not seem to have Bloom Reader running:");
                foreach (var deviceName in deviceNames)
                {
                    _progress.MessageWithoutLocalizing($"\t{deviceName}");
                }
                break;

            case DeviceNotFoundReportType.MoreThanOneReadyDevice:
                _progress.Message(id: "MoreThanOne",
                                  message: "The following connected devices all have Bloom Reader installed. Please connect only one of these devices.");
                foreach (var deviceName in deviceNames)
                {
                    _progress.MessageWithoutLocalizing($"\t{deviceName}");
                }
                break;
            }
        }
        /// <summary>
        /// Check for either "Device16x9Portrait" or "Device16x9Landscape" layout.
        /// Complain to the user if another layout is currently chosen.
        /// </summary>
        /// <remarks>
        /// See https://issues.bloomlibrary.org/youtrack/issue/BL-5274.
        /// </remarks>
        public static void CheckBookLayout(Bloom.Book.Book book, WebSocketProgress progress)
        {
            var layout            = book.GetLayout();
            var desiredLayoutSize = "Device16x9";

            if (layout.SizeAndOrientation.PageSizeName != desiredLayoutSize)
            {
                // The progress object has been initialized to use an id prefix.  So we'll access L10NSharp explicitly here.  We also want to make the string blue,
                // which requires a special argument.
//				var msgFormat = L10NSharp.LocalizationManager.GetString("Common.Note",
//					"Note", "A heading shown above some messages.");
//				progress.MessageWithoutLocalizing(msgFormat, MessageKind.Note);
                var msgFormat = L10NSharp.LocalizationManager.GetString("PublishTab.Android.WrongLayout.Message",
                                                                        "The layout of this book is currently \"{0}\". Bloom Reader will display it using \"{1}\", so text might not fit. To see if anything needs adjusting, go back to the Edit Tab and change the layout to \"{1}\".",
                                                                        "{0} and {1} are book layout tags.");
                var desiredLayout = desiredLayoutSize + layout.SizeAndOrientation.OrientationName;
                var msg           = String.Format(msgFormat, layout.SizeAndOrientation.ToString(), desiredLayout, Environment.NewLine);
                progress.MessageWithoutLocalizing(msg, MessageKind.Note);
            }
        }
        /// <summary>
        /// This is the core of sending a book to a device. We need a book and a bookServer in order to come up
        /// with the .bloomd file.
        /// We are either simply saving the .bloomd to destFileName, or else we will make a temporary .bloomd file and
        /// actually send it using sendAction.
        /// We report important progress on the progress control. This includes reporting that we are starting
        /// the actual transmission using startingMessageAction, which is passed the safe file name (for checking pre-existence
        /// in UsbPublisher) and the book title (typically inserted into the message).
        /// If a confirmAction is passed (currently only by UsbPublisher), we use it check for a successful transfer
        /// before reporting completion (except for file save, where the current message is inappropriate).
        /// This is an awkward case where the three ways of publishing are similar enough that
        /// it's annoying and dangerous to have three entirely separate methods but somewhat awkward to combine them.
        /// Possibly we could eventually make them more similar, e.g., it would simplify things if they all said
        /// "Sending X to Y", though I'm not sure that would be good i18n if Y is sometimes a device name
        /// and sometimes a path.
        /// </summary>
        /// <param name="book"></param>
        /// <param name="destFileName"></param>
        /// <param name="sendAction"></param>
        /// <param name="progress"></param>
        /// <param name="bookServer"></param>
        /// <param name="startingMessageFunction"></param>
        public static void SendBook(Book.Book book, BookServer bookServer, string destFileName, Action <string, string> sendAction, WebSocketProgress progress, Func <string, string, string> startingMessageFunction,
                                    Func <string, bool> confirmFunction, Color backColor, AndroidPublishSettings settings = null)
        {
            var bookTitle = book.Title;

            progress.MessageUsingTitle("PackagingBook", "Packaging \"{0}\" for use with Bloom Reader...", bookTitle, MessageKind.Progress);

            // compress audio if needed, with progress message
            if (AudioProcessor.IsAnyCompressedAudioMissing(book.FolderPath, book.RawDom))
            {
                progress.Message("CompressingAudio", "Compressing audio files");
                AudioProcessor.TryCompressingAudioAsNeeded(book.FolderPath, book.RawDom);
            }
            var publishedFileName = bookTitle + BookCompressor.ExtensionForDeviceBloomBook;

            if (startingMessageFunction != null)
            {
                progress.MessageWithoutLocalizing(startingMessageFunction(publishedFileName, bookTitle));
            }
            if (destFileName == null)
            {
                // wifi or usb...make the .bloomd in a temp folder.
                using (var bloomdTempFile = TempFile.WithFilenameInTempFolder(publishedFileName))
                {
                    BloomReaderFileMaker.CreateBloomDigitalBook(bloomdTempFile.Path, book, bookServer, backColor, progress, settings);
                    sendAction(publishedFileName, bloomdTempFile.Path);
                    if (confirmFunction != null && !confirmFunction(publishedFileName))
                    {
                        throw new ApplicationException("Book does not exist after write operation.");
                    }
                    progress.MessageUsingTitle("BookSent", "You can now read \"{0}\" in Bloom Reader!", bookTitle, MessageKind.Note);
                }
            }
            else
            {
                // save file...user has supplied name, there is no further action.
                Debug.Assert(sendAction == null, "further actions are not supported when passing a path name");
                BloomReaderFileMaker.CreateBloomDigitalBook(destFileName, book, bookServer, backColor, progress, settings);
                progress.Message("PublishTab.Epub.Done", "Done", useL10nIdPrefix: false);                       // share message string with epub publishing
            }
        }
Пример #9
0
        public void RegisterWithApiHandler(BloomApiHandler apiHandler)
        {
            // This is just for storing the user preference of method
            // If we had a couple of these, we could just have a generic preferences api
            // that browser-side code could use.
            apiHandler.RegisterEndpointLegacy(kApiUrlPart + "method", request =>
            {
                if (request.HttpMethod == HttpMethods.Get)
                {
                    var method = Settings.Default.PublishAndroidMethod;
                    if (!new string[] { "wifi", "usb", "file" }.Contains(method))
                    {
                        method = "wifi";
                    }
                    request.ReplyWithText(method);
                }
                else                 // post
                {
                    Settings.Default.PublishAndroidMethod = request.RequiredPostString();
#if __MonoCS__
                    if (Settings.Default.PublishAndroidMethod == "usb")
                    {
                        _progress.MessageWithoutLocalizing("Sorry, this method is not available on Linux yet.");
                    }
#endif
                    request.PostSucceeded();
                }
            }, true);

            apiHandler.RegisterEndpointLegacy(kApiUrlPart + "backColor", request =>
            {
                if (request.HttpMethod == HttpMethods.Get)
                {
                    if (request.CurrentBook != _coverColorSourceBook)
                    {
                        _coverColorSourceBook = request.CurrentBook;
                        ImageUtils.TryCssColorFromString(request.CurrentBook?.GetCoverColor() ?? "", out _thumbnailBackgroundColor);
                    }
                    request.ReplyWithText(ToCssColorString(_thumbnailBackgroundColor));
                }
                else                 // post
                {
                    // ignore invalid colors (very common while user is editing hex)
                    Color newColor;
                    var newColorAsString = request.RequiredPostString();
                    if (ImageUtils.TryCssColorFromString(newColorAsString, out newColor))
                    {
                        _thumbnailBackgroundColor = newColor;
                        request.CurrentBook.SetCoverColor(newColorAsString);
                    }
                    request.PostSucceeded();
                }
            }, true);

            apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "motionBookMode",
                                                      readRequest =>
            {
                // If the user has taken off all possible motion, force not having motion in the
                // Bloom Reader book.  See https://issues.bloomlibrary.org/youtrack/issue/BL-7680.
                if (!readRequest.CurrentBook.HasMotionPages)
                {
                    readRequest.CurrentBook.BookInfo.PublishSettings.BloomPub.Motion = false;
                }
                return(readRequest.CurrentBook.BookInfo.PublishSettings.BloomPub.Motion);
            },
                                                      (writeRequest, value) =>
            {
                writeRequest.CurrentBook.BookInfo.PublishSettings.BloomPub.Motion = value;
                writeRequest.CurrentBook.BookInfo.SavePublishSettings();
                _webSocketServer.SendEvent("publish", "motionChanged");
            }
                                                      , true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "updatePreview", request =>
            {
                MakeBloompubPreview(request, false);
            }, false);


            apiHandler.RegisterEndpointLegacy(kApiUrlPart + "thumbnail", request =>
            {
                var coverImage = request.CurrentBook.GetCoverImagePath();
                if (coverImage == null)
                {
                    request.Failed("no cover image");
                }
                else
                {
                    // We don't care as much about making it resized as making its background transparent.
                    using (var thumbnail = TempFile.CreateAndGetPathButDontMakeTheFile())
                    {
                        if (_thumbnailBackgroundColor == Color.Transparent)
                        {
                            ImageUtils.TryCssColorFromString(request.CurrentBook?.GetCoverColor(), out _thumbnailBackgroundColor);
                        }
                        RuntimeImageProcessor.GenerateEBookThumbnail(coverImage, thumbnail.Path, 256, 256, _thumbnailBackgroundColor);
                        request.ReplyWithImage(thumbnail.Path);
                    }
                }
            }, true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "usb/start", request =>
            {
#if !__MonoCS__
                SetState("UsbStarted");
                UpdatePreviewIfNeeded(request);
                _usbPublisher.Connect(request.CurrentBook, _thumbnailBackgroundColor, GetSettings());
#endif
                request.PostSucceeded();
            }, true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "usb/stop", request =>
            {
#if !__MonoCS__
                _usbPublisher.Stop(disposing: false);
                SetState("stopped");
#endif
                request.PostSucceeded();
            }, true);
            apiHandler.RegisterEndpointLegacy(kApiUrlPart + "wifi/start", request =>
            {
                SetState("ServingOnWifi");
                UpdatePreviewIfNeeded(request);
                _wifiPublisher.Start(request.CurrentBook, request.CurrentCollectionSettings, _thumbnailBackgroundColor, GetSettings());

                request.PostSucceeded();
            }, true);

            apiHandler.RegisterEndpointLegacy(kApiUrlPart + "wifi/stop", request =>
            {
                _wifiPublisher.Stop();
                SetState("stopped");
                request.PostSucceeded();
            }, true);

            apiHandler.RegisterEndpointLegacy(kApiUrlPart + "file/save", request =>
            {
                UpdatePreviewIfNeeded(request);
                FilePublisher.Save(request.CurrentBook, _bookServer, _thumbnailBackgroundColor, _progress, GetSettings());
                SetState("stopped");
                request.PostSucceeded();
            }, true);

            apiHandler.RegisterEndpointLegacy(kApiUrlPart + "file/bulkSaveBloomPubsParams", request =>
            {
                request.ReplyWithJson(JsonConvert.SerializeObject(_collectionSettings.BulkPublishBloomPubSettings));
            }, true);

            apiHandler.RegisterEndpointLegacy(kApiUrlPart + "file/bulkSaveBloomPubs", request =>
            {
                // update what's in the collection so that we remember for next time
                _collectionSettings.BulkPublishBloomPubSettings = request.RequiredPostObject <BulkBloomPubPublishSettings>();
                _collectionSettings.Save();

                _bulkBloomPubCreator.PublishAllBooks(_collectionSettings.BulkPublishBloomPubSettings);
                SetState("stopped");
                request.PostSucceeded();
            }, true);

            apiHandler.RegisterEndpointLegacy(kApiUrlPart + "textToClipboard", request =>
            {
                PortableClipboard.SetText(request.RequiredPostString());
                request.PostSucceeded();
            }, true);

            apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "canHaveMotionMode",
                                                      request =>
            {
                return(request.CurrentBook.HasMotionPages);
            },
                                                      null,  // no write action
                                                      false,
                                                      true); // we don't really know, just safe default

            apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "canRotate",
                                                      request =>
            {
                return(request.CurrentBook.BookInfo.PublishSettings.BloomPub.Motion && request.CurrentBook.HasMotionPages);
            },
                                                      null,  // no write action
                                                      false,
                                                      true); // we don't really know, just safe default

            apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "defaultLandscape",
                                                      request =>
            {
                return(request.CurrentBook.GetLayout().SizeAndOrientation.IsLandScape);
            },
                                                      null,  // no write action
                                                      false,
                                                      true); // we don't really know, just safe default
            apiHandler.RegisterEndpointLegacy(kApiUrlPart + "languagesInBook", request =>
            {
                try
                {
                    InitializeLanguagesInBook(request);

                    Dictionary <string, InclusionSetting> textLangsToPublish  = request.CurrentBook.BookInfo.PublishSettings.BloomPub.TextLangs;
                    Dictionary <string, InclusionSetting> audioLangsToPublish = request.CurrentBook.BookInfo.PublishSettings.BloomPub.AudioLangs;

                    var result = "[" + string.Join(",", _allLanguages.Select(kvp =>
                    {
                        string langCode = kvp.Key;

                        bool includeText = false;
                        if (textLangsToPublish != null && textLangsToPublish.TryGetValue(langCode, out InclusionSetting includeTextSetting))
                        {
                            includeText = includeTextSetting.IsIncluded();
                        }

                        bool includeAudio = false;
                        if (audioLangsToPublish != null && audioLangsToPublish.TryGetValue(langCode, out InclusionSetting includeAudioSetting))
                        {
                            includeAudio = includeAudioSetting.IsIncluded();
                        }

                        var value = new LanguagePublishInfo()
                        {
                            code             = kvp.Key,
                            name             = request.CurrentBook.PrettyPrintLanguage(langCode),
                            complete         = kvp.Value,
                            includeText      = includeText,
                            containsAnyAudio = _languagesWithAudio.Contains(langCode),
                            includeAudio     = includeAudio
                        };
                        var json = JsonConvert.SerializeObject(value);
                        return(json);
                    })) + "]";

                    request.ReplyWithText(result);
                }
        private void MakeAceByDaisyReport(ApiRequest request)
        {
            // First check whether ace has been installed.
            var daisyDirectory = FindAceByDaisyOrTellUser(request);             // this method does the request.fail() if needed

            if (string.IsNullOrEmpty(daisyDirectory))
            {
                return;
            }
            // As of version 1.0.2, the ace report has stylesheets on the internet.  See https://issues.bloomlibrary.org/youtrack/issue/BL-6118.
            // To be specific, https://cdn.datatables.net/1.10.15/css/dataTables.bootstrap4.min.css and
            // https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css.
            if (!UrlLookup.CheckGeneralInternetAvailability(true))
            {
                _webSocketProgress.ErrorWithoutLocalizing(
                    "Sorry, you must have an internet connection in order to view the Ace by DAISY report.");
                request.Failed();
                return;
            }

            var reportRootDirectory = Path.Combine(System.IO.Path.GetTempPath(), "daisy-ace-reports");

            // Do our best at clearing out previous runs.
            // This call is ok if the directory does not exist at all.
            SIL.IO.RobustIO.DeleteDirectoryAndContents(reportRootDirectory);
            // This call is ok if the above failed and it still exists
            Directory.CreateDirectory(reportRootDirectory);

            // was having a problem with some files from previous reports getting locked.
            // so give new folder names if needed
            var haveReportedError = false;
            var errorMessage      = "Unknown Error";

            var version = GetAceByDaisyVersion(daisyDirectory);

            if (version.old)
            {
                _webSocketProgress.MessageWithoutLocalizing(
                    $"You appear to have an older version ({version.version}) of ACE by Daisy. This may cause problems.",
                    ProgressKind.Warning);
                _webSocketProgress.MessageWithoutLocalizing(
                    "We recommend you run \"npm install @daisy/ace -g\" from a command line to get the latest.",
                    ProgressKind.Warning);
            }

            var started = DateTime.Now;

            var epubPath = MakeEpub(reportRootDirectory, _webSocketProgress);

            // Try 3 times. It could be that this is no longer needed, but working on a developer
            // machine isn't proof.
            for (var i = 0; i < 3; i++)
            {
                var randomName      = Guid.NewGuid().ToString();
                var reportDirectory = Path.Combine(reportRootDirectory, randomName);

                var       arguments             = $"ace.js --verbose -o \"{reportDirectory}\" \"{epubPath}\"";
                const int kSecondsBeforeTimeout = 60;
                var       progress = new NullProgress();
                _webSocketProgress.MessageWithoutLocalizing("Running Ace by DAISY");

                ExecutionResult res    = null;
                string          ldpath = null;
                try
                {
                    // Without this variable switching on Linux, the chrome inside ace finds the
                    // wrong version of a library as part of our mozilla code.
                    ldpath = Environment.GetEnvironmentVariable("LD_LIBRARY_PATH");
                    Environment.SetEnvironmentVariable("LD_LIBRARY_PATH", null);
                    res = CommandLineRunner.Run("node", arguments, Encoding.UTF8, daisyDirectory, kSecondsBeforeTimeout,
                                                progress,
                                                (dummy) => { });
                }
                finally
                {
                    // Restore the variable for our next geckofx browser to find.
                    if (!String.IsNullOrEmpty(ldpath))
                    {
                        Environment.SetEnvironmentVariable("LD_LIBRARY_PATH", ldpath);
                    }
                }

                if (res.DidTimeOut)
                {
                    errorMessage = $"Daisy Ace timed out after {kSecondsBeforeTimeout} seconds.";
                    _webSocketProgress.ErrorWithoutLocalizing(errorMessage);
                    continue;
                }

                var answerPath = Path.Combine(reportDirectory, "report.html");
                if (!File.Exists(answerPath))
                {
                    // This hasn't been effectively reproduced, but there was a case where this would fail at least
                    // half the time on a book, reproducable. That book had 2 pages pointing at placeholder.png,
                    // and we were getting an error related to it being locked. So we deduce that ace was trying
                    // to copy the file twice, at the same time (normal nodejs code is highly async).
                    // Now the problem is not reproducable, but I'm leaving in this code that tried to deal with it.
                    errorMessage = $"Exit code{res.ExitCode}{Environment.NewLine}" +
                                   $"Standard Error{Environment.NewLine}{res.StandardError}{Environment.NewLine}" +
                                   $"Standard Out{res.StandardOutput}";

                    _webSocketProgress.ErrorWithoutLocalizing(errorMessage);

                    continue;                     // something went wrong, try again
                }

                // The html client is set to treat a text reply as a url of the report. Make sure it's valid for being a URL.
                // See https://silbloom.myjetbrains.com/youtrack/issue/BL-6197.
                request.ReplyWithText("/bloom/" + answerPath.EscapeFileNameForHttp());
                if (version.old)
                {
                    // If we displayed an important message about updating ACE, make sure the user
                    // has SOME time to see it.
                    // Review: is this long enough? Should we look for some other way to show the
                    // problem? At present the problems caused by an old version (1.1 is the oldest
                    // contemporary with Bloom) is fairly minor.
                    while (DateTime.Now - started < new TimeSpan(0, 0, 0, 5))
                    {
                        Thread.Sleep(100);
                    }
                }
                return;
            }

            // If we get this far, we give up.
            ReportErrorAndFailTheRequest(request, errorMessage);
        }
Пример #11
0
        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);
        }
        public static void DoWorkWithProgressDialog(BloomWebSocketServer socketServer, string socketContext, Func <Form> makeDialog,
                                                    Func <IWebSocketProgress, bool> doWhat, Action <Form> doWhenMainActionFalse = null)
        {
            var progress = new WebSocketProgress(socketServer, socketContext);

            // NOTE: This (specifically ShowDialog) blocks the main thread until the dialog is closed.
            // Be careful to avoid deadlocks.
            using (var dlg = makeDialog())
            {
                // For now let's not try to handle letting the user abort.
                dlg.ControlBox = false;
                var worker = new BackgroundWorker();
                worker.DoWork += (sender, args) =>
                {
                    // A way of waiting until the dialog is ready to receive progress messages
                    while (!socketServer.IsSocketOpen(socketContext))
                    {
                        Thread.Sleep(50);
                    }
                    bool waitForUserToCloseDialogOrReportProblems;
                    try
                    {
                        waitForUserToCloseDialogOrReportProblems = doWhat(progress);
                    }
                    catch (Exception ex)
                    {
                        // depending on the nature of the problem, we might want to do more or less than this.
                        // But at least this lets the dialog reach one of the states where it can be closed,
                        // and gives the user some idea things are not right.
                        socketServer.SendEvent(socketContext, "finished");
                        waitForUserToCloseDialogOrReportProblems = true;
                        progress.MessageWithoutLocalizing("Something went wrong: " + ex.Message, ProgressKind.Error);
                    }

                    // stop the spinner
                    socketServer.SendEvent(socketContext, "finished");
                    if (waitForUserToCloseDialogOrReportProblems)
                    {
                        // Now the user is allowed to close the dialog or report problems.
                        // (ProgressDialog in JS-land is watching for this message, which causes it to turn
                        // on the buttons that allow the dialog to be manually closed (or a problem to be reported).
                        socketServer.SendBundle(socketContext, "show-buttons", new DynamicJson());
                    }
                    else
                    {
                        // Just close the dialog
                        dlg.Invoke((Action)(() =>
                        {
                            if (doWhenMainActionFalse != null)
                            {
                                doWhenMainActionFalse(dlg);
                            }
                            else
                            {
                                dlg.Close();
                            }
                        }));
                    }
                };

                worker.RunWorkerAsync();
                dlg.ShowDialog();                 // returns when dialog closed
            }
        }
Пример #13
0
        public void RegisterWithApiHandler(BloomApiHandler apiHandler)
        {
            // This is just for storing the user preference of method
            // If we had a couple of these, we could just have a generic preferences api
            // that browser-side code could use.
            apiHandler.RegisterEndpointHandler(kApiUrlPart + "method", request =>
            {
                if (request.HttpMethod == HttpMethods.Get)
                {
                    var method = Settings.Default.PublishAndroidMethod;
                    if (!new string[] { "wifi", "usb", "file" }.Contains(method))
                    {
                        method = "wifi";
                    }
                    request.ReplyWithText(method);
                }
                else                 // post
                {
                    Settings.Default.PublishAndroidMethod = request.RequiredPostString();
#if __MonoCS__
                    if (Settings.Default.PublishAndroidMethod == "usb")
                    {
                        _progress.MessageWithoutLocalizing("Sorry, this method is not available on Linux yet.");
                    }
#endif
                    request.PostSucceeded();
                }
            }, true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "backColor", request =>
            {
                if (request.HttpMethod == HttpMethods.Get)
                {
                    if (request.CurrentBook != _coverColorSourceBook)
                    {
                        _coverColorSourceBook = request.CurrentBook;
                        ImageUtils.TryCssColorFromString(request.CurrentBook?.GetCoverColor() ?? "", out _thumbnailBackgroundColor);
                    }
                    request.ReplyWithText(ToCssColorString(_thumbnailBackgroundColor));
                }
                else                 // post
                {
                    // ignore invalid colors (very common while user is editing hex)
                    Color newColor;
                    var newColorAsString = request.RequiredPostString();
                    if (ImageUtils.TryCssColorFromString(newColorAsString, out newColor))
                    {
                        _thumbnailBackgroundColor = newColor;
                        request.CurrentBook.SetCoverColor(newColorAsString);
                    }
                    request.PostSucceeded();
                }
            }, true);

            apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "motionBookMode",
                                                      readRequest =>
            {
                // If the user has taken off all possible motion, force not having motion in the
                // Bloom Reader book.  See https://issues.bloomlibrary.org/youtrack/issue/BL-7680.
                if (!readRequest.CurrentBook.HasMotionPages)
                {
                    readRequest.CurrentBook.MotionMode = false;
                }
                return(readRequest.CurrentBook.MotionMode);
            },
                                                      (writeRequest, value) =>
            {
                writeRequest.CurrentBook.MotionMode = value;
            }
                                                      , true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "updatePreview", request =>
            {
                if (request.HttpMethod == HttpMethods.Post)
                {
                    // This is already running on a server thread, so there doesn't seem to be any need to kick off
                    // another background one and return before the preview is ready. But in case something in C#
                    // might one day kick of a new preview, or we find we do need a background thread,
                    // I've made it a websocket broadcast when it is ready.
                    // If we've already left the publish tab...we can get a few of these requests queued up when
                    // a tester rapidly toggles between views...abandon the attempt
                    if (!PublishHelper.InPublishTab)
                    {
                        request.Failed("aborted, no longer in publish tab");
                        return;
                    }
                    try
                    {
                        UpdatePreview(request);
                        request.PostSucceeded();
                    }
                    catch (Exception e)
                    {
                        request.Failed("Error while updating preview. Message: " + e.Message);
                        NonFatalProblem.Report(ModalIf.Alpha, PassiveIf.All, "Error while updating preview.", null, e, true);
                    }
                }
            }, false);


            apiHandler.RegisterEndpointHandler(kApiUrlPart + "thumbnail", request =>
            {
                var coverImage = request.CurrentBook.GetCoverImagePath();
                if (coverImage == null)
                {
                    request.Failed("no cover image");
                }
                else
                {
                    // We don't care as much about making it resized as making its background transparent.
                    using (var thumbnail = TempFile.CreateAndGetPathButDontMakeTheFile())
                    {
                        if (_thumbnailBackgroundColor == Color.Transparent)
                        {
                            ImageUtils.TryCssColorFromString(request.CurrentBook?.GetCoverColor(), out _thumbnailBackgroundColor);
                        }
                        RuntimeImageProcessor.GenerateEBookThumbnail(coverImage, thumbnail.Path, 256, 256, _thumbnailBackgroundColor);
                        request.ReplyWithImage(thumbnail.Path);
                    }
                }
            }, true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "usb/start", request =>
            {
#if !__MonoCS__
                SetState("UsbStarted");
                UpdatePreviewIfNeeded(request);
                _usbPublisher.Connect(request.CurrentBook, _thumbnailBackgroundColor, GetSettings());
#endif
                request.PostSucceeded();
            }, true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "usb/stop", request =>
            {
#if !__MonoCS__
                _usbPublisher.Stop();
                SetState("stopped");
#endif
                request.PostSucceeded();
            }, true);
            apiHandler.RegisterEndpointHandler(kApiUrlPart + "wifi/start", request =>
            {
                SetState("ServingOnWifi");
                UpdatePreviewIfNeeded(request);
                _wifiPublisher.Start(request.CurrentBook, request.CurrentCollectionSettings, _thumbnailBackgroundColor, GetSettings());

                request.PostSucceeded();
            }, true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "wifi/stop", request =>
            {
                _wifiPublisher.Stop();
                SetState("stopped");
                request.PostSucceeded();
            }, true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "file/save", request =>
            {
                UpdatePreviewIfNeeded(request);
                FilePublisher.Save(request.CurrentBook, _bookServer, _thumbnailBackgroundColor, _progress, GetSettings());
                SetState("stopped");
                request.PostSucceeded();
            }, true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "cleanup", request =>
            {
                Stop();
                request.PostSucceeded();
            }, true);

            apiHandler.RegisterEndpointHandler(kApiUrlPart + "textToClipboard", request =>
            {
                PortableClipboard.SetText(request.RequiredPostString());
                request.PostSucceeded();
            }, true);

            apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "canHaveMotionMode",
                                                      request =>
            {
                return(request.CurrentBook.HasMotionPages);
            },
                                                      null,  // no write action
                                                      false,
                                                      true); // we don't really know, just safe default

            apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "canRotate",
                                                      request =>
            {
                return(request.CurrentBook.MotionMode && request.CurrentBook.HasMotionPages);
            },
                                                      null,  // no write action
                                                      false,
                                                      true); // we don't really know, just safe default

            apiHandler.RegisterBooleanEndpointHandler(kApiUrlPart + "defaultLandscape",
                                                      request =>
            {
                return(request.CurrentBook.GetLayout().SizeAndOrientation.IsLandScape);
            },
                                                      null,  // no write action
                                                      false,
                                                      true); // we don't really know, just safe default
            apiHandler.RegisterEndpointHandler(kApiUrlPart + "languagesInBook", request =>
            {
                try
                {
                    InitializeLanguagesInBook(request);

                    Dictionary <string, InclusionSetting> textLangsToPublish  = request.CurrentBook.BookInfo.MetaData.TextLangsToPublish.ForBloomPUB;
                    Dictionary <string, InclusionSetting> audioLangsToPublish = request.CurrentBook.BookInfo.MetaData.AudioLangsToPublish.ForBloomPUB;

                    var result = "[" + string.Join(",", _allLanguages.Select(kvp =>
                    {
                        string langCode = kvp.Key;

                        bool includeText = false;
                        if (textLangsToPublish != null && textLangsToPublish.TryGetValue(langCode, out InclusionSetting includeTextSetting))
                        {
                            includeText = includeTextSetting.IsIncluded();
                        }

                        bool includeAudio = false;
                        if (audioLangsToPublish != null && audioLangsToPublish.TryGetValue(langCode, out InclusionSetting includeAudioSetting))
                        {
                            includeAudio = includeAudioSetting.IsIncluded();
                        }

                        var value = new LanguagePublishInfo()
                        {
                            code             = kvp.Key,
                            name             = request.CurrentBook.PrettyPrintLanguage(langCode),
                            complete         = kvp.Value,
                            includeText      = includeText,
                            containsAnyAudio = _languagesWithAudio.Contains(langCode),
                            includeAudio     = includeAudio
                        };
                        var json = JsonConvert.SerializeObject(value);
                        return(json);
                    })) + "]";

                    request.ReplyWithText(result);
                }
        public void RegisterWithServer(EnhancedImageServer server)
        {
            // This is just for storing the user preference of method
            // If we had a couple of these, we could just have a generic preferences api
            // that browser-side code could use.
            server.RegisterEndpointHandler(kApiUrlPart + "method", request =>
            {
                if (request.HttpMethod == HttpMethods.Get)
                {
                    var method = Settings.Default.PublishAndroidMethod;
                    if (!new string[] { "wifi", "usb", "file" }.Contains(method))
                    {
                        method = "wifi";
                    }
                    request.ReplyWithText(method);
                }
                else                 // post
                {
                    Settings.Default.PublishAndroidMethod = request.RequiredPostString();
#if __MonoCS__
                    if (Settings.Default.PublishAndroidMethod == "usb")
                    {
                        _progress.MessageWithoutLocalizing("Sorry, this method is not available on Linux yet.");
                    }
#endif
                    request.PostSucceeded();
                }
            }, true);

            server.RegisterEndpointHandler(kApiUrlPart + "backColor", request =>
            {
                if (request.HttpMethod == HttpMethods.Get)
                {
                    if (request.CurrentBook != _coverColorSourceBook)
                    {
                        _coverColorSourceBook = request.CurrentBook;
                        TryCssColorFromString(request.CurrentBook?.GetCoverColor() ?? "", out _thumbnailBackgroundColor);
                    }
                    request.ReplyWithText(ToCssColorString(_thumbnailBackgroundColor));
                }
                else                 // post
                {
                    // ignore invalid colors (very common while user is editing hex)
                    Color newColor;
                    var newColorAsString = request.RequiredPostString();
                    if (TryCssColorFromString(newColorAsString, out newColor))
                    {
                        _thumbnailBackgroundColor = newColor;
                        request.CurrentBook.SetCoverColor(newColorAsString);
                    }
                    request.PostSucceeded();
                }
            }, true);


            server.RegisterEndpointHandler(kApiUrlPart + "photoStoryMode", request =>
            {
                if (request.HttpMethod == HttpMethods.Get)
                {
                    // this is temporary, just trying to get support for full screen pan & zoom out quickly in 4.2
                    request.ReplyWithText(request.CurrentBook.UsePhotoStoryModeInBloomReader.ToString()
                                          .ToLowerInvariant()); // "false", not "False"
                }
                else                                            // post
                {
                    request.CurrentBook.UsePhotoStoryModeInBloomReader = bool.Parse(request.RequiredPostString());
                    request.PostSucceeded();
                }
            }, true);


            server.RegisterEndpointHandler(kApiUrlPart + "thumbnail", request =>
            {
                var coverImage = request.CurrentBook.GetCoverImagePath();
                if (coverImage == null)
                {
                    request.Failed("no cover image");
                }
                else
                {
                    // We don't care as much about making it resized as making its background transparent.
                    using (var thumbnail = TempFile.CreateAndGetPathButDontMakeTheFile())
                    {
                        if (_thumbnailBackgroundColor == Color.Transparent)
                        {
                            TryCssColorFromString(request.CurrentBook?.GetCoverColor(), out _thumbnailBackgroundColor);
                        }
                        RuntimeImageProcessor.GenerateEBookThumbnail(coverImage, thumbnail.Path, 256, 256, _thumbnailBackgroundColor);
                        request.ReplyWithImage(thumbnail.Path);
                    }
                }
            }, true);

            server.RegisterEndpointHandler(kApiUrlPart + "usb/start", request =>
            {
#if !__MonoCS__
                SetState("UsbStarted");
                _usbPublisher.Connect(request.CurrentBook, _thumbnailBackgroundColor);
#endif
                request.PostSucceeded();
            }, true);

            server.RegisterEndpointHandler(kApiUrlPart + "usb/stop", request =>
            {
#if !__MonoCS__
                _usbPublisher.Stop();
                SetState("stopped");
#endif
                request.PostSucceeded();
            }, true);
            server.RegisterEndpointHandler(kApiUrlPart + "wifi/start", request =>
            {
                _wifiPublisher.Start(request.CurrentBook, request.CurrentCollectionSettings, _thumbnailBackgroundColor);
                SetState("ServingOnWifi");
                request.PostSucceeded();
            }, true);

            server.RegisterEndpointHandler(kApiUrlPart + "wifi/stop", request =>
            {
                _wifiPublisher.Stop();
                SetState("stopped");
                request.PostSucceeded();
            }, true);

            server.RegisterEndpointHandler(kApiUrlPart + "file/save", request =>
            {
                FilePublisher.Save(request.CurrentBook, _bookServer, _thumbnailBackgroundColor, _progress);
                SetState("stopped");
                request.PostSucceeded();
            }, true);

            server.RegisterEndpointHandler(kApiUrlPart + "cleanup", request =>
            {
                Stop();
                request.PostSucceeded();
            }, true);

            server.RegisterEndpointHandler(kApiUrlPart + "textToClipboard", request =>
            {
                PortableClipboard.SetText(request.RequiredPostString());
                request.PostSucceeded();
            }, true);
        }