How does FBReader open a given ebook and implement some reading operations

First of all, let’s review some of the points from the previous article. For an identifiable valid ebook file:

  • The ebook file in the phone’s store is created as a file object of type ZLPhysicalFile via zlFile.createFilebypath
  • Most of the methods for BookCollectionShadow are actually implemented by BookCollection
  • One of the most important methods in BookCollection is getBookByFile (ZLFile), which verifies the extension of the file and gets the corresponding parsing plug-in if it is a supported ebook format
  • Then create a DbBook object in the BookCollection. When the DbBook is initialized, the basic information of the book is read by calling the native method of the plugin passed in
  • Book information page open FBReader reading through FBReaderIntents. PutBookExtra (intent, book), pass a valid parameters book object

How does FBReader get the book and display the book contents

How does FBReader access books and make it easier to open an ebook

Looking at the manifest file, we can see the startup mode of FBReader:

android:launchMode="singleTask"
Copy the code

If FBReader is opened again, its onNewIntent will be triggered:

@Override protected void onNewIntent(final Intent intent) { final String action = intent.getAction(); final Uri data = intent.getData(); if ((intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) ! = 0) { super.onNewIntent(intent); } else if (Intent.ACTION_VIEW.equals(action) && data ! = null && "fbreader-action".equals(data.getScheme())) {// Ignore some code... } else if (Intent) ACTION_VIEW) equals (action) | | FBReaderIntents. Action. The equals (action)) {/ / for myOpenBookIntent assignment myOpenBookIntent = intent; // Ignore some code... } else if (FBReaderIntents. Action. The PLUGIN. Equals (Action)) {/ / ignore the part of the code... } else if (intent.action_search.equals (action)) { } else if (FBReaderIntents. Action. The CLOSE. The equals (intent. The getAction ())) {/ / ignore the part of the code... } else if (FBReaderIntents. Action. PLUGIN_CRASH. Equals (intent. The getAction ())) {/ / ignore the part of the code... } else { super.onNewIntent(intent); }}Copy the code

The Intent validates the action. What was the action of the Intent? Here’s a look back at the code that was called when the reading page was opened:

FBReader.openBookActivity(BookInfoActivity.this, myBook, null); public static void openBookActivity(Context context, Book book, Bookmark bookmark) { final Intent intent = defaultIntent(context); FBReaderIntents.putBookExtra(intent, book); FBReaderIntents.putBookmarkExtra(intent, bookmark); context.startActivity(intent); } public static Intent defaultIntent(Context context) { return new Intent(context, FBReader. Class). SetAction (FBReaderIntents. Action. The VIEW). / / set the Action addFlags (Intent. FLAG_ACTIVITY_CLEAR_TOP); }Copy the code

The Intent of the action is set by default to FBReaderIntents. Action. The VIEW, so in onNewIntent method, after the breakpoint can know, in VIEW of the current us jump over to read from the books information, I’m just doing my assignment to myOpenBookIntent, nothing else.

In that case, we’ll move on to FBReader’s onResume:

@Override protected void onResume() { super.onResume(); // Ignore some code... if (myCancelIntent ! = null) {// Ignore some code... } else if (myOpenBookIntent ! = null) { final Intent intent = myOpenBookIntent; myOpenBookIntent = null; getCollection().bindToService(this, new Runnable() { public void run() { openBook(intent, null, true); }}); } else if (myFBReaderApp.getCurrentServerBook(null) ! = null) {// Ignore some code... } else if (myFBReaderApp.Model == null && myFBReaderApp.ExternalBook ! = null) {// Ignore some code... } else {// Ignore some code... }}Copy the code

When myOpenBookIntent! = null, getCollection().bindToService is executed.

private BookCollectionShadow getCollection() {
    return (BookCollectionShadow)myFBReaderApp.Collection;
}
Copy the code

BookCollectionShadow (); runnable (openBook);

private synchronized void openBook(Intent intent, final Runnable action, boolean force) { if (! force && myBook ! = null) { return; } / / take out the book myBook = FBReaderIntents. GetBookExtra (intent, myFBReaderApp. Collection); final Bookmark bookmark = FBReaderIntents.getBookmarkExtra(intent); if (myBook == null) { final Uri data = intent.getData(); if (data ! = null) { myBook = createBookForFile(ZLFile.createFileByPath(data.getPath())); }} // Ignore some code... Config.Instance().runOnConnect(new Runnable() { public void run() { myFBReaderApp.openBook(myBook, bookmark, action, myNotifier); AndroidFontUtil.clearFontCache(); }}); }Copy the code

In the openBook method, we found the book we passed in earlier. If the Intent does not pass book, but does pass a Uri, call createBookForFile:

private Book createBookForFile(ZLFile file) { if (file == null) { return null; } Book book = myFBReaderApp.Collection.getBookByFile(file.getPath()); if (book ! = null) { return book; } if (file.isArchive()) { for (ZLFile child : file.children()) { book = myFBReaderApp.Collection.getBookByFile(child.getPath()); if (book ! = null) { return book; } } } return null; }Copy the code

Familiar method to create a Book. In this case, we can also open an e-book in this way:

Public static void openBookActivity(Context Context, String path) { final Intent intent = FBReader.defaultIntent(context); intent.setData(Uri.parse(path)); context.startActivity(intent); }Copy the code

About Book and DbBook

BookCollectionShadow and BookCollection, both of which are derived from AbstractBookCollection, But BookCollectionShadow is the Book that is used, and BookCollection is the DbBook that is used:

public class BookCollectionShadow extends AbstractBookCollection<Book> implements ServiceConnection

public class BookCollection extends AbstractBookCollection<DbBook>
Copy the code

Let’s look at the definitions of the Book and DbBook classes:

public final class DbBook extends AbstractBook

public final class Book extends AbstractBook
Copy the code

So these two classes, obviously, are subclasses that both inherit from AbstractBook, but we looked at BookCollectionShadow and there’s an implementation of IBookCollection, and it’s actually BookCollection, But they are based on two different data types, such as getBookByFile:

Public synchronized Book getBookByFile(String path) {if (myInterface == null) {return null; } try { return SerializerUtil.deserializeBook(myInterface.getBookByFile(path), this); } catch (RemoteException e) { return null; Public DbBook getBookByFile(String path) {return getBookByFile(zlfile.createFilebypath (path)); }Copy the code

BookCollectionShadow calls getBookByFile expecting data of type Book, while the implementer calls getBookByFile and returns data of type DbBook.

In BookCollectionShadow, we can find that eventually return is SerializerUtil deserializeBook method returns the data. So what does this method do? Click inside to have a look:

SerializerUtil.class 

private static final AbstractSerializer defaultSerializer = new XMLSerializer();

public static <B extends AbstractBook> B deserializeBook(String xml, AbstractSerializer.BookCreator<B> creator) {
    return xml != null ? defaultSerializer.deserializeBook(xml, creator) : null;
}

XMLSerializer.class
@Override
public <B extends AbstractBook> B deserializeBook(String xml, BookCreator<B> creator) {
    try {
        final BookDeserializer<B> deserializer = new BookDeserializer<B>(creator);
        Xml.parse(xml, deserializer);
        return deserializer.getBook();
    } catch (SAXException e) {
        System.err.println(xml);
        e.printStackTrace();
        return null;
    }
}
Copy the code

When the getBookByFile method of BookCollectionShadow is called, the getBookByFile of LibraryService is called, which returns a piece of XML data. The BookCollectionShadow will parse this XML data into the corresponding Book object. We know that even though BookCollection is the ultimate implementer, there is a LibraryImplementation in the LibraryService that acts as an intermediary between BookCollectionShadow and BookCollectionShadow, So let’s look at what the middleman does with this method:

Public String getBookByFile(String path) {myCollection is an instance of BookCollection, Return results for DbBook return SerializerUtil. Serialize (myCollection. GetBookByFile (path)); }Copy the code

Also in SerializerUtil:

public static String serialize(AbstractBook book) { return book ! = null ? defaultSerializer.serialize(book) : null; } XMLSerializer.class @Override public String serialize(AbstractBook book) { final StringBuilder buffer = builder(); serialize(buffer, book); return buffer.toString(); }Copy the code

We won’t go into the details, but the process is already clear. Take getBookByFile as an example:

  • The client calls this method through the BookCollectionShadow instance with the intention of getting data of type Book
  • BookCollectionShadow calls the getBookByFile method of the middleman LibraryImplementation
  • LibraryImplementation calls the getBookByFile method of the ultimate implementer BookCollection, which returns DbBook data
  • LibraryImplementation converts the returned DbBook to the corresponding XML data through SerializerUtil
  • The transformed XML is returned to the client BookCollectionShadow and is again turned into a Book object via SerializerUtil

AbstractBook and its parent classes do not implement Serializable or Parcelable. Some of the core information about the Book is passed to the client through cross-process transfer, while the client can ignore other dataBase operations in DbBook.

Book gets content and prepares for display

FBReader () : FBReader () : FBReader () : FBReader () : FBReader ()

This starts with the last piece of code in the openBook method:

Private synchronized void openBook(Intent Intent, final Runnable action, Boolean force) {private synchronized void openBook(Intent Intent, final Runnable Action, Boolean force) { Config.Instance().runOnConnect(new Runnable() { public void run() { myFBReaderApp.openBook(myBook, bookmark, action, myNotifier); AndroidFontUtil.clearFontCache(); }}); }Copy the code

The runOnConnect method, which we have examined before, will execute runnable. Here, we see the introduction of a new character, the FBReaderApp.

See when the FBReaderApp was initialized in the FBReader:

@Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); // Ignore some code... myFBReaderApp = (FBReaderApp)FBReaderApp.Instance(); if (myFBReaderApp == null) { myFBReaderApp = new FBReaderApp(Paths.systemInfo(this), new BookCollectionShadow()); } myFBReaderApp.setWindow(this); // Ignore some code... }Copy the code

When FBReader is first entered, fbreaderapp.instance () is null, it is created with new and reused later. Take a look at how it is constructed:

public FBReaderApp(SystemInfo systemInfo, final IBookCollection<Book> collection) 
Copy the code

BookCollectionShadow we are familiar with BookCollectionShadow. What is SystemInfo? Check it out:

public interface SystemInfo {
    String tempDirectory();
    String networkCacheDirectory();
}
Copy the code

Look at the paths.systeminfo passed in when onCreate created FBReaderApp:

public static SystemInfo systemInfo(Context context) {
    final Context appContext = context.getApplicationContext();
    return new SystemInfo() {
        public String tempDirectory() {
            final String value = ourTempDirectoryOption.getValue();
            if (!"".equals(value)) {
                return value;
            }
            return internalTempDirectoryValue(appContext);
        }

        public String networkCacheDirectory() {
            return tempDirectory() + "/cache";
        }
    };
}
Copy the code

Looks like it’s getting file storage and cache paths.

Next, let’s go to FBReaderApp and take a look at its openBook method:

Public void openBook(Book Book, final Bookmark Bookmark, Runnable postAction, Notifier Notifier) {// Ignore some code.. final SynchronousExecutor executor = createExecutor("loadingBook"); executor.execute(new Runnable() { public void run() { openBookInternal(bookToOpen, bookmark, false); } }, postAction); }Copy the code

Step by step analysis:

1. CreateExecutor:

protected SynchronousExecutor createExecutor(String key) {
    if (myWindow != null) {
        return myWindow.createExecutor(key);
    } else {
        return myDummyExecutor;
    }
}
Copy the code

SetWindow (this); fBreaderApp.setwindow (this); myWindow (FBReader); createExecutor (FBReaderApp);

@Override
public FBReaderApp.SynchronousExecutor createExecutor(String key) {
    return UIUtil.createExecutor(this, key);
}
Copy the code

Then we go to UIUtil:

public static ZLApplication.SynchronousExecutor createExecutor(final Activity activity, final String key) { return new ZLApplication.SynchronousExecutor() { private final ZLResource myResource = ZLResource.resource("dialog").getResource("waitMessage"); private final String myMessage = myResource.getResource(key).getValue(); private volatile ProgressDialog myProgress; public void execute(final Runnable action, final Runnable uiPostAction) { activity.runOnUiThread(new Runnable() { public void run() { myProgress = ProgressDialog.show(activity, null, myMessage, true, false); final Thread runner = new Thread() { public void run() { action.run(); activity.runOnUiThread(new Runnable() { public void run() { try { myProgress.dismiss(); myProgress = null; } catch (Exception e) { e.printStackTrace(); } if (uiPostAction ! = null) { uiPostAction.run(); }}}); }}; runner.setPriority(Thread.MAX_PRIORITY); runner.start(); }}); } // Ignore some code... }; }Copy the code

A quick look at what this code does:

  • Load resource ZLResource
  • Obtain the resource information MSG under resouce based on the incoming key
  • Implement the execute (action, uiaction)
  • Create the ProgressDialog while executing and set its prompt to MSG
  • The child thread is then created to execute the action, and when it is finished, the Activity is scheduled to the main thread to close the ProgressDialog, and uiAction is then executed

So what is the resource information? And where is it stored? To answer these two questions, we need to take a look at ZLResource:

static void buildTree() {
    synchronized (ourLock) {
        if (ourRoot == null) {
            ourRoot = new ZLTreeResource("", null);
            ourLanguage = "en";
            ourCountry = "UK";
            loadData();
        }
    }
}

private static void loadData() {
	ResourceTreeReader reader = new ResourceTreeReader();
	loadData(reader, ourLanguage + ".xml");
	loadData(reader, ourLanguage + "_" + ourCountry + ".xml");
}

private static void loadData(ResourceTreeReader reader, String fileName) {
	reader.readDocument(ourRoot, ZLResourceFile.createResourceFile("resources/zlibrary/" + fileName));
	reader.readDocument(ourRoot, ZLResourceFile.createResourceFile("resources/application/" + fileName));
	reader.readDocument(ourRoot, ZLResourceFile.createResourceFile("resources/lang.xml"));
	reader.readDocument(ourRoot, ZLResourceFile.createResourceFile("resources/application/neutral.xml"));
}
Copy the code

ZLResource will first buildTree before loading resources, loadData method will be called in buildTree for the first time, and finally load the resource files of the current system language in the resource directory, respectively the corresponding language resource files under Zlibrary. The corresponding language resource file under application, lang resource file, application/neutral.

Chinese System Resource File:

The UIUtil called by the above analysis loads “dialog”-“waitMessage”-“loadingBook” respectively.

2.openBookInternal(bookToOpen, bookmark, false)

Through the first step of analysis, when execute is called, the first runnable, which is the openBookInternal method, is executed:

Private synchronized void openBookInternal(Final Book Book, Bookmark Bookmark, Boolean force) {private synchronized void openBookInternal(final Book Book, Bookmark Bookmark, Boolean force) final PluginCollection pluginCollection = PluginCollection.Instance(SystemInfo); final FormatPlugin plugin; try { plugin = BookUtil.getPlugin(pluginCollection, book); } catch (BookReadingException e) { processException(e); return; } // Ignore some code... try { Model = BookModel.createModel(book, plugin); Collection.saveBook(book); ZLTextHyphenator.Instance().load(book.getLanguage()); BookTextView.setModel(Model.getTextModel()); setBookmarkHighlightings(BookTextView, null); gotoStoredPosition(); if (bookmark == null) { setView(BookTextView); } else { gotoBookmark(bookmark, false); } Collection.addToRecentlyOpened(book); final StringBuilder title = new StringBuilder(book.getTitle()); if (! book.authors().isEmpty()) { boolean first = true; for (Author a : book.authors()) { title.append(first ? "(" : ", "); title.append(a.DisplayName); first = false; } title.append(")"); } setTitle(title.toString()); } catch (BookReadingException e) { processException(e); } getViewWidget().reset(); getViewWidget().repaint(); // Ignore some code... }Copy the code

About PluginCollection. The Instance (SystemInfo) :

public static PluginCollection Instance(SystemInfo systemInfo) {
    if (ourInstance == null) {
        createInstance(systemInfo);
    }
    return ourInstance;
}

private static synchronized void createInstance(SystemInfo systemInfo) {
	if (ourInstance == null) {
		ourInstance = new PluginCollection(systemInfo);
		// This code cannot be moved to constructor
		// because nativePlugins() is a native method
		for (NativeFormatPlugin p : ourInstance.nativePlugins(systemInfo)) {
			ourInstance.myBuiltinPlugins.add(p);
			System.err.println("native plugin: " + p);
		}
	}
}

private native NativeFormatPlugin[] nativePlugins(SystemInfo systemInfo);
Copy the code

After the initialization of PluginCollection, native nativePlugins are called to obtain a collection of book parsing plug-ins, and the returned result is the corresponding parsing plug-ins of each parsable e-book type. Here, the e-book format I opened is Epub, and the plug-in I obtained is OEBNativePlugin:

Next we look at this method, bookutil.getPlugin (pluginCollection, book), which was analyzed in the previous article, where we finally get the parsing plug-in corresponding to the ebook format by differentiating the book file type.

Then a super-core approach came along! That’s how you parse the content of an e-book:

BookModel.createModel(book, plugin); BookModel.class public static BookModel createModel(Book book, FormatPlugin plugin) throws BookReadingException { if (plugin instanceof BuiltinFormatPlugin) { final BookModel model = new BookModel(book); ((BuiltinFormatPlugin)plugin).readModel(model); return model; } throw new BookReadingException( "unknownPluginType", null, new String[] { String.valueOf(plugin) } ); } For the books I tested, NativeFormatPlugin's readModel synchronized public void readModel(BookModel Model) throws BookReadingException when the book content is finally parsed { final int code; final String tempDirectory = SystemInfo.tempDirectory(); Synchronized (ourNativeLock) {synchronized (ourNativeLock) {readModelNative(model, tempDirectory); } switch (code) { case 0: return; case 3: throw new CachedCharStorageException( "Cannot write file from native code to " + tempDirectory ); default: throw new BookReadingException( "nativeCodeFailure", BookUtil.fileByBook(model.Book), new String[] { String.valueOf(code), model.Book.getPath() } ); } } private native int readModelNative(BookModel model, String cacheDir);Copy the code

BookMode content before parsing:

The parsed contents of BookMode:

Finally, let’s look at the last two sentences:

getViewWidget().reset(); getViewWidget().repaint(); public final ZLViewWidget getViewWidget() { return myWindow ! = null ? myWindow.getViewWidget() : null; } We know that myWindow is FBReader, so we can look at the getViewWidget in FBReader:  @Override public ZLViewWidget getViewWidget() { return myMainView; } in FBReader onCreate: myMainView = (ZLAndroidWidget)findViewById(R.id.main_view);Copy the code

Go to the ZLAndroidWidget to see the corresponding method:

@Override public void reset() { myBitmapManager.reset(); } @Override public void repaint() { postInvalidate(); } BitmapManagerImpl.class void reset() { for (int i = 0; i < SIZE; ++i) { myIndexes[i] = null; }}Copy the code

Finally, the page maps the contents of the ebook.

Of course, due to my limited time in contact with this project, and my lack of experience in writing technical articles, there will inevitably be mistakes, unclear descriptions, language overload and other problems in the process, I hope you can understand, but also hope that you continue to give correction. Finally, THANK you for your support to me, so that I have a strong motivation to stick to it.

PS: Exploring the Art of Android Development, the first line of the introduction: “As things stand,Android development is hot…” . See this sentence, eyes full of tears ah! Youth! How did it go so fast! .