FBReader打开书柜,找到手机中存储的文件并将其打开
通过上一篇分析,我们已经知道如何响应并打开菜单,而且菜单中第一项是打开本地书柜,这一篇我们就以此为入口,去探究FBReader的书柜是怎么实现,以及是如何分辨一本书并且能打开一本书的。
一、打开FBReader本地书柜时,首页内容显示都做了些什么
打开本地书柜action:ShowLibraryAction,直接看其run方法:
@Override
protected void run(Object ... params) {
final Intent externalIntent =
new Intent(FBReaderIntents.Action.EXTERNAL_LIBRARY);
final Intent internalIntent =
new Intent(BaseActivity.getApplicationContext(), LibraryActivity.class);
//查询是否有满足条件的插件书柜,有则打开插件中的书柜,没有就打开本地书柜 LibraryActivity
if (PackageUtil.canBeStarted(BaseActivity, externalIntent, true)) {
try {
startLibraryActivity(externalIntent);
} catch (ActivityNotFoundException e) {
startLibraryActivity(internalIntent);
}
} else {
startLibraryActivity(internalIntent);
}
}
复制代码
该Activity为ListActivity:
查看其onCreate:
private final BookCollectionShadow myCollection = new BookCollectionShadow();
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
// 忽略部分代码...
new LibraryTreeAdapter(this);
myCollection.bindToService(this, new Runnable() {
public void run() {
setProgressBarIndeterminateVisibility(!myCollection.status().IsComplete);
myRootTree = new RootTree(myCollection, PluginCollection.Instance(Paths.systemInfo(LibraryActivity.this)));
myCollection.addListener(LibraryActivity.this);
init(getIntent());
}
});
}
复制代码
可以发现,这里设置了adapter为LibraryTreeAdapter,那么这个页面显示的内容数据是从哪来的呢?别着急,这里我们先去看一下myCollection是个什么东东,我们就顺着onCreate中调用的bindToService看起:
public synchronized boolean bindToService(Context context, Runnable onBindAction) {
if (myInterface != null && myContext == context) {
if (onBindAction != null) {
Config.Instance().runOnConnect(onBindAction);
}
return true;
} else {
if (onBindAction != null) {
synchronized (myOnBindActions) {
myOnBindActions.add(onBindAction);
}
}
final boolean result = context.bindService(
FBReaderIntents.internalIntent(FBReaderIntents.Action.LIBRARY_SERVICE),
this,
Service.BIND_AUTO_CREATE
);
if (result) {
myContext = context;
}
return result;
}
}
复制代码
可以看出,这个collection会绑定LibraryService,而且传递的ServiceConnection为this,那么说明这个collection实现了ServiceConnection这个接口,顺着这个逻辑去看一下onServiceConnected:
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (this) {
myInterface = LibraryInterface.Stub.asInterface(service);
}
final List<Runnable> actions;
synchronized (myOnBindActions) {
actions = new ArrayList<Runnable>(myOnBindActions);
myOnBindActions.clear();
}
for (Runnable a : actions) {
Config.Instance().runOnConnect(a);
}
// 忽略部分代码...
}
复制代码
通过分析这两个方法可以得知,bindToService(context,runnable)是当已绑定service时,则直接通过Config来执行runnable,否则先绑定service,然后再通过Config来执行runnable。那么这个Config又是何方神圣呢?我们就顺着其runOnConnect来一探究竟:
public abstract void runOnConnect(Runnable runnable);
复制代码
向下查找,可以找到其唯一实现在子类ConfigShadow中:
@Override
public void runOnConnect(Runnable runnable) {
if (myInterface != null) {//当前已成功绑定service,则直接执行run
runnable.run();
} else {
synchronized (myDeferredActions) {
myDeferredActions.add(runnable);//未成功绑定放入集合中,待执行
}
}
}
复制代码
再来看一下ConfigShadow的构造方法:
public ConfigShadow(Context context) {
myContext = context;
context.bindService(
FBReaderIntents.internalIntent(FBReaderIntents.Action.CONFIG_SERVICE),
this,
Service.BIND_AUTO_CREATE
);
}
复制代码
可以看出该类绑定了ConfigService,而且自身实现了ServiceConnection接口:
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (this) {
myInterface = ConfigInterface.Stub.asInterface(service);
myContext.registerReceiver(
myReceiver, new IntentFilter(FBReaderIntents.Event.CONFIG_OPTION_CHANGE)
);
}
final List<Runnable> actions;
synchronized (myDeferredActions) {
actions = new ArrayList<Runnable>(myDeferredActions);
myDeferredActions.clear();
}
for (Runnable a : actions) {
a.run();
}
}
复制代码
可以看出,在成功链接ConfigService之后,首先绑定了Config_Option_Change的广播接收者,并且将待执行集合中的runnable依次取出并执行。
那么ConfigService是做啥的呢?直接进去看看:
public class ConfigService extends Service {
private ConfigInterface.Stub myConfig;
@Override
public IBinder onBind(Intent intent) {
return myConfig;
}
@Override
public void onCreate() {
super.onCreate();
myConfig = new SQLiteConfig(this);
}
// 忽略部分代码
}
复制代码
可以看到onBind返回的是SQLiteConfig,这里也就比较明确了,是操作Config数据库的。那么ConfigShadow又是在哪被创建的呢,通过find可以查到其唯一创建在ZLAndroidApplication:
public abstract class ZLAndroidApplication extends Application {
// 忽略部分代码
private ConfigShadow myConfig;
@Override
public void onCreate() {
super.onCreate();
myConfig = new ConfigShadow(this);
}
}
复制代码
到此,我们先简单总结一下:
- 应用在启动时通过创建ConfigShadow绑定了ConfigService(进程:configService)
- LibraryActivity在启动时通过BookCollectionShadow绑定了LibraryService(进程:libraryService)
- BookCollectionShadow的bindToService(context,runnable)方法,在绑定LibraryService成功后,会通过Config的runOnConnect方法处理runnable
通过以上简单的总结,我们可以知道,下一步一般会执行runnable中的方法:
myRootTree = new RootTree(myCollection, PluginCollection.Instance(Paths.systemInfo(LibraryActivity.this)));
myCollection.addListener(LibraryActivity.this);
init(getIntent());
复制代码
那么我们就一步步来分析,这些操作都做了些什么:
1.RootTree的创建:
public RootTree(IBookCollection collection, PluginCollection pluginCollection) {
super(collection, pluginCollection);
// 收藏
new FavoritesTree(this);
// 最近读过
new RecentBooksTree(this);
// 作者
new AuthorListTree(this);
// 按书名
new TitleListTree(this);
// 按系列
new SeriesListTree(this);
// 标签
new TagListTree(this);
// 同步
if (new SyncOptions().Enabled.getValue()) {
new SyncTree(this);
}
// 文件夹
new FileFirstLevelTree(this);
}
复制代码
FBReader对于树形结构通过ZLTree来定义。
看到这里,我们应该可以判断,书库首页打开时显示的数据就来自于此,那么这些数据又是怎么样传递给Adapter的呢?看样还得继续看。
2.BookCollectionShadow添加监听,这个我们后面再分析。
3.调用init(intent)方法:
//TreeActivity
protected void init(Intent intent) {
//首次打开书库时intent中没有数据,key为null
final FBTree.Key key = (FBTree.Key)intent.getSerializableExtra(TREE_KEY_KEY);
final FBTree.Key selectedKey = (FBTree.Key)intent.getSerializableExtra(SELECTED_TREE_KEY_KEY);
//根据指定的key获取对应的tree
myCurrentTree = getTreeByKey(key);
myCurrentKey = myCurrentTree.getUniqueKey();
final TreeAdapter adapter = getTreeAdapter();
adapter.replaceAll(myCurrentTree.subtrees(), false);
// 忽略部分代码...
}
//LibraryActivity
@Override
protected LibraryTree getTreeByKey(FBTree.Key key) {
return key != null ? myRootTree.getLibraryTree(key) : myRootTree;
}
//TreeAdapter LibraryTreeAdapter父类
public void replaceAll(final Collection<FBTree> items, final boolean invalidateViews) {
myActivity.runOnUiThread(new Runnable() {
public void run() {
synchronized (myItems) {
myItems.clear();
myItems.addAll(items);
}
notifyDataSetChanged();
if (invalidateViews) {
myActivity.getListView().invalidateViews();
}
}
});
}
复制代码
通过上面三个方法的,我们可以知道,在起初进入LibraryAcivity时,由于intent中没有数据,故取出的key为null,在调用getTreeByKey时返回RootTree,并通过Adapter的replaceAll方法,修改数据并nofity。
二、揭开BookCollectionShadow的神秘面纱
通过上面的分析,我们已经知道BookCollectionShadow是用来绑定LibraryService的。通过它,可以跟LibraryService进行通讯。另外,上面还有提到注册监听者,这个监听者又是怎么一回事,LibraryService又是怎么一回事呢?我们接下来就一步步去弄清这些问题。
先来看一下它的继承关系图:
由于BookCollectionShadow实现了IBookCollection接口,而该接口定义行为与LibraryService.aidl一致,并且BookCollectionShadow是用来绑定LibraryService的,细看其对故其对IBookCollection的实现均会通过LibraryInterface实例由进程间通讯,调用LibraryService的对应方法。
那么我们就去看一下,这个LibraryService到底是个做啥的:
public class LibraryService extends Service
复制代码
1.onCreate:
@Override
public void onCreate() {
super.onCreate();
synchronized (ourDatabaseLock) {
if (ourDatabase == null) {
ourDatabase = new SQLiteBooksDatabase(LibraryService.this);
}
}
myLibrary = new LibraryImplementation(ourDatabase);
bindService(
new Intent(this, DataService.class),
DataConnection,
DataService.BIND_AUTO_CREATE
);
}
复制代码
可以看出这里做了三件事:
- 创建了SQLiteBooksDatabase,该数据库为Book数据库
- 创建了LibraryImplementation实例myLibrary,并把数据库传递进去
- 绑定了DataService(进程:dataService)
2.onBind:
@Override
public IBinder onBind(Intent intent) {
return myLibrary;
}
复制代码
从这里可以看出,返回的IBinder对象为myLibrary,那么也就意味着LibraryImplementation继承自LibraryService.Stub,也就具体实现了LibraryInterface。
3.LibraryImplementation:
public final class LibraryImplementation extends LibraryInterface.Stub {
private final BooksDatabase myDatabase;
private final List<FileObserver> myFileObservers = new LinkedList<FileObserver>();
private BookCollection myCollection;
LibraryImplementation(BooksDatabase db) {
myDatabase = db;
myCollection = new BookCollection(
Paths.systemInfo(LibraryService.this), myDatabase, Paths.bookPath()
);
reset(true);
}
//忽略部分代码...
}
复制代码
这里发现实例化了一个名字不带Shadow的BookCollection,这个BookCollection又是干啥的呢?
4.BookCollection:
public class BookCollection extends AbstractBookCollection<DbBook>
对比看一下 BookCollectionShadow
public class BookCollectionShadow extends AbstractBookCollection<Book> implements ServiceConnection
复制代码
通过继承关系,我们可以得知BookCollection跟BookCollectionShadow一样是继承自AbstractBookCollection,只不过泛型的AbstractBook类型不同。而且通过上面BookCollectionShadow的继承图来看,可以推出BookCollection同样实现了IBookCollection接口。
5.由于LibraryService在onBind时返回的实例为LibraryImplementation,所以在BookCollectionShadow中:
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (this) {
// LibraryInterface的实例myInterface,实际为LibraryService中的LibraryImplementation
myInterface = LibraryInterface.Stub.asInterface(service);
}
// 忽略部分代码...
}
复制代码
通过查看代码,可以看出BookCollectionShadow对接口IBookCollection的实现具体均是通过myInterface调用同样的方法跨进程完成,我们随便挑一个实现方法看一下:
public synchronized void rescan(String path) {
if (myInterface != null) {
try {
myInterface.rescan(path);
} catch (RemoteException e) {
// ignore
}
}
}
复制代码
6.myInterface的真身LibraryImplementation对LibraryInterface的具体实现,同5我们还是查看rescan:
public void rescan(String path) {
// myCollection为BookCollection的实例
myCollection.rescan(path);
}
复制代码
这里就很明显了,LibraryImplementation对LibraryInterface的具体实现,最后的执行主体就是BookCollection。
到此,我们就可以简单对BookCollectionShadow做一个总结:
- 绑定服务LibraryService
- 实现IBookCollection,与服务LibraryService有同样的功能
- 绑定LibraryService成功时,获取到的LibraryInterface实例为LibraryImplementation
- 调用IBookCollection定义的方法时,实际是由LibraryImplementation调用内部的BookCollection实例来最终完成
- BookCollection行为基本以通过SQLiteBooksDatabase操作Book.db为主
BookCollectionShadow是BookCollection的“影子”,真正的实现是BookCollection。
三、书柜页面LibraryActivity,是如何响应不同类型item点击的
上面我们已经知道,书柜首页的数据来自于RootTree,接下来我们就从“文件夹”这个item,继续往下分析。
既然要查看item的点击处理,那就查看点击处理方法onListItemClick:
@Override
protected void onListItemClick(ListView listView, View view, int position, long rowId) {
final LibraryTree tree = (LibraryTree)getTreeAdapter().getItem(position);
if (tree instanceof ExternalViewTree) {
runOrInstallExternalView(true);
} else {
final Book book = tree.getBook();
if (book != null) {
showBookInfo(book);
} else {
openTree(tree);
}
}
}
复制代码
一个判断book != null,非空打开书籍详情,为空就打开子树。从RootTree初始化中,我们知道“文件夹”对应的Tree为FileFirstLevelTree:
其getBook方法通过追溯可以发现是在LibraryTree中,而且return null。那么就是打开子tree,具体是通过方法openTree:
private void openTree(final FBTree tree, final FBTree treeToSelect, final boolean storeInHistory) {
switch (tree.getOpeningStatus()) {
case WAIT_FOR_OPEN:
case ALWAYS_RELOAD_BEFORE_OPENING:
final String messageKey = tree.getOpeningStatusMessage();
if (messageKey != null) {
UIUtil.createExecutor(TreeActivity.this, messageKey).execute(
new Runnable() {
public void run() {
tree.waitForOpening();
}
},
new Runnable() {
public void run() {
openTreeInternal(tree, treeToSelect, storeInHistory);
}
}
);
} else {
// 对于FileFirstLevelTree来说会执行这块
tree.waitForOpening();
openTreeInternal(tree, treeToSelect, storeInHistory);
}
break;
default:
openTreeInternal(tree, treeToSelect, storeInHistory);
break;
}
}
复制代码
对于FileFirstLevelTree来说:
@Override
public Status getOpeningStatus() {
return Status.ALWAYS_RELOAD_BEFORE_OPENING;
}
public String getOpeningStatusMessage() {
return null;
}
复制代码
那么上面的openTree会执行标记处的代码,也就是会执行waitForOpening方法:
@Override
public void waitForOpening() {
clear();
for (String dir : Paths.BookPathOption.getValue()) {
addChild(dir, resource().getResource("fileTreeLibrary").getValue(), dir);
}
addChild("/", "fileTreeRoot");
final List<String> cards = Paths.allCardDirectories();
if (cards.size() == 1) {
addChild(cards.get(0), "fileTreeCard");
} else {
final ZLResource res = resource().getResource("fileTreeCard");
final String title = res.getResource("withIndex").getValue();
final String summary = res.getResource("summary").getValue();
int index = 0;
for (String dir : cards) {
addChild(dir, title.replaceAll("%s", String.valueOf(++index)), summary);
}
}
}
复制代码
这段代码可以暂时不去关心具体都是做了哪些,我们只需要知道最终经过waitForOpening这个方法,他生成了当前tree的子tree,那么子tree又是如何生成的呢?这个我们就要去着重看一下方法addChild:
private void addChild(String path, String title, String summary) {
final ZLFile file = ZLFile.createFileByPath(path);
if (file != null) {
new FileTree(this, file, title, summary);
}
}
复制代码
这里我们不免会有个疑惑,既然是add打头的方法,为什么跟到最后,发现,并没有哪里进行了相关add操作呢?其实,这个add是发生在了FileTree创建的时候:
FileTree(LibraryTree parent, ZLFile file, String name, String summary) {
super(parent);
//忽略部分代码
}
一直往上追溯,查看super构造方法,最终在ZLTree:
protected ZLTree(T parent, int position) {
//忽略部分代码...
Parent = parent;
if (parent != null) {
Level = parent.Level + 1;
//此处执行add操作,将this当前tree,添加到parent中
parent.addSubtree((T)this, position);
} else {
Level = 0;
}
}
复制代码
通过执行waitForOpening,已经准备好了当前tree的子tree,那么就进入到后续显示子tree的处理了:
private void openTreeInternal(FBTree tree, FBTree treeToSelect, boolean storeInHistory) {
switch (tree.getOpeningStatus()) {
case READY_TO_OPEN:
case ALWAYS_RELOAD_BEFORE_OPENING:
if (storeInHistory && !myCurrentKey.equals(tree.getUniqueKey())) {
myHistory.add(myCurrentKey);
}
onNewIntent(new Intent(this, getClass())
.setAction(OPEN_TREE_ACTION)
.putExtra(TREE_KEY_KEY, tree.getUniqueKey())
.putExtra(
SELECTED_TREE_KEY_KEY,
treeToSelect != null ? treeToSelect.getUniqueKey() : null
)
.putExtra(HISTORY_KEY, new ArrayList<FBTree.Key>(myHistory))
);
break;
case CANNOT_OPEN:
UIMessageUtil.showErrorMessage(TreeActivity.this, tree.getOpeningStatusMessage());
break;
}
}
复制代码
这里同样的会执行ALWAYS_RELOAD_BEFORE_OPENING这个case,也就是说会调用onNewIntent,并且在intent中传递了key为TREE_KEY_KEY,value为tree.getUniqueKey()的参数。接着我们来看一下onNewIntent:
@Override
protected void onNewIntent(final Intent intent) {
OrientationUtil.setOrientation(this, intent);
if (OPEN_TREE_ACTION.equals(intent.getAction())) {
runOnUiThread(new Runnable() {
public void run() {
init(intent);
}
});
} else {
super.onNewIntent(intent);
}
}
复制代码
上面分析时,我们知道在调用方法init(intent)时,会读取intent中的数据,这时key不为空,可以根据key获得对应的tree,并且再次调用adapter的replaceAll替换数据并notify。
到这里,我们来个简单的总结。
当点击的item不是book时:
- 调用该tree的waitForOpening方法,准备子tree
- 在生成子tree时,将父tree作为构造参数传入子tree,会将子tree添加到父tree的subtree中
- 调用onNewIntent方法,并将点击的tree.getgetUniqueKey传入intent中
- Activity的onNewIntent方法出发,会执行init(intent)
- init(intent)方法会读取传递过来的key,根据此key获取到对应的tree
- 调用adapter的replcaAll方法,将adapter的数据替换为获取到的tree.subTrees
当点击的item为book时,打开图书详情。
四、FBReader在扫描本地文件时,是如何识别出可阅读电子书的
通过上面的分析,我们已经知道,当我们点击非图书item时,都是经历了些什么。那么当我们继续往下点,进入SD卡目录去扫描其中文件时,可以发现当FBReader扫描到某个item是单子书时会显示出来电子书的图标,那么FBReader是如何识别的呢?
由书柜首页“电子书”,点击进入子tree时,通过上面的分析,我们知道其准备子tree时调用了addChild方法,而其中生成的子tree为FileTree:
private void addChild(String path, String title, String summary) {
final ZLFile file = ZLFile.createFileByPath(path);
if (file != null) {
new FileTree(this, file, title, summary);
}
}
复制代码
这里调用了一个非常重要的方法ZLFile.createFileByPath(path):
public static ZLFile createFileByPath(String path) {
if (path == null) {
return null;
}
ZLFile cached = ourCachedFiles.get(path);
if (cached != null) {
//缓存中有,则直接返回缓存中的ZLFile
return cached;
}
int len = path.length();
char first = len == 0 ? '*' : path.charAt(0);
if (first != '/') {
//路径为资源路径时
while (len > 1 && first == '.' && path.charAt(1) == '/') {
path = path.substring(2);
len -= 2;
first = len == 0 ? '*' : path.charAt(0);
}
return ZLResourceFile.createResourceFile(path);
}
int index = path.lastIndexOf(':');
if (index > 1) {
//路径中包含:时
final ZLFile archive = createFileByPath(path.substring(0, index));
if (archive != null && archive.myArchiveType != 0) {
return ZLArchiveEntryFile.createArchiveEntryFile(
archive, path.substring(index + 1)
);
}
}
return new ZLPhysicalFile(path);
}
复制代码
我们知道SD卡中的文件夹或文件路径以“/”开头,而且不包含“:”,那么在生成子tree时,就会对应生成ZLPhysicalFile。这里我们也就知道了ZLPhysicalFile是用来描述物理文件的,而且FBReader中对文件的描述,统一是基于ZLFile。接着,我们就进入ZLPhysicalFile,去了解一下这个类具体是做啥的:
private final File myFile;//路径对应的文件实体
ZLPhysicalFile(String path) {
this(new File(path));
}
public ZLPhysicalFile(File file) {
myFile = file;
init();
}
protected void init() {
final String name = getLongName();
final int index = name.lastIndexOf('.');
//获取文件拓展名
myExtension = (index > 0) ? name.substring(index + 1).toLowerCase().intern() : "";
myShortName = name.substring(name.lastIndexOf('/') + 1);
int archiveType = ArchiveType.NONE;
//根据特定扩展名,给文件设置archiveType
if (myExtension == "zip") {
archiveType |= ArchiveType.ZIP;
} else if (myExtension == "oebzip") {
archiveType |= ArchiveType.ZIP;
} else if (myExtension == "epub") {
archiveType |= ArchiveType.ZIP;
} else if (myExtension == "tar") {
archiveType |= ArchiveType.TAR;
} else if (lowerCaseName.endsWith(".tgz")) {
//nothing to-do myNameWithoutExtension = myNameWithoutExtension.substr(0, myNameWithoutExtension.length() - 3) + "tar";
//myArchiveType = myArchiveType | ArchiveType.TAR | ArchiveType.GZIP;
}
myArchiveType = archiveType;
}
复制代码
这里ZLPhysicalFile在初始化时做了两件事:
- 保存路径对应的真实File
- 对文件进行识别,看其是否属于支持的电子书格式类型,如果是的话,则将其archiveType设置为对应值
这样我们也就能够看出,如果遍历到的文件为特定格式的电子书文件时,其archiveType是会有对应值的。那么就可以猜测是不是在LibraryActivity的Adapter中应该是有对该值的判断?进去看一下:
public View getView(int position, View convertView, final ViewGroup parent) {
final LibraryTree tree = (LibraryTree)getItem(position);
//忽略部分代码...
if (myCoverManager == null) {
view.measure(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
final int coverHeight = view.getMeasuredHeight();
final TreeActivity activity = getActivity();
myCoverManager = new CoverManager(activity, activity.ImageSynchronizer, coverHeight * 15 / 32, coverHeight);
view.requestLayout();
}
final ImageView coverView = ViewUtil.findImageView(view, R.id.library_tree_item_icon);
if (!myCoverManager.trySetCoverImage(coverView, tree)) {
coverView.setImageResource(getCoverResourceId(tree));
}
return view;
}
private int getCoverResourceId(LibraryTree tree) {
if (tree.getBook() != null) {
return R.drawable.ic_list_library_book;
} else if (tree instanceof ExternalViewTree) {
return R.drawable.plugin_bookshelf;
} else if (tree instanceof FavoritesTree) {
return R.drawable.ic_list_library_favorites;
} else if (tree instanceof FileTree) {
final ZLFile file = ((FileTree)tree).getFile();
if (file.isArchive()) {
return R.drawable.ic_list_library_zip;
} else if (file.isDirectory() && file.isReadable()) {
return R.drawable.ic_list_library_folder;
} else {
return R.drawable.ic_list_library_permission_denied;
}
}...
}
复制代码
发现在Adapter的getView方法中,并没有对archiveType的判断,但是却发现:
- 这里会创建封面管理器CoverManager
- 并且当调用myCoverManager.trySetCoverImage返回false的时候,才会给imageview设置图片资源
- getCoverResourceId方法是根据当前tree的类型来给它设置对应图片资源的
那么这个trySetCoverImage方法又做了什么呢?
public boolean trySetCoverImage(ImageView coverView, FBTree tree) {
final CoverHolder holder = getHolder(coverView, tree);
Bitmap coverBitmap;
try {
//取缓存中的封面bitmap
coverBitmap = Cache.getBitmap(holder.Key);
} catch (CoverCache.NullObjectException e) {
return false;
}
if (coverBitmap == null) {
final ZLImage cover = tree.getCover();
if (cover instanceof ZLImageProxy) {
final ZLImageProxy img = (ZLImageProxy)cover;
if (img.isSynchronized()) {
setCoverForView(holder, img);
} else {
img.startSynchronization(
myImageSynchronizer,
holder.new CoverSyncRunnable(img)
);
}
} else if (cover != null) {
coverBitmap = getBitmap(cover);
}
}
if (coverBitmap != null) {
//如果封面coverBitmap存在,就设置给Imageview
holder.CoverView.setImageBitmap(coverBitmap);
return true;
}
return false;
}
复制代码
其中对是否有封面的一个核心方法是tree.getCover():
public final ZLImage getCover() {
if (!myCoverRequested) {
//生成封面
myCover = createCover();
//忽略部分代码...
}
return myCover;
}
@Override
public ZLImage createCover() {
return CoverUtil.getCover(getBook(), PluginCollection);
}
复制代码
最终是调用了CoverUtil的getConver的方法:
public static ZLImage getCover(AbstractBook book, IFormatPluginCollection collection) {
if (book == null) {
return null;
}
synchronized (book) {
return getCover(ZLFile.createFileByPath(book.getPath()), collection);
}
}
复制代码
所以当book不为空时,才会执行获取封面的方法,那么就需要看一下getBook获取的book对象是否为空:
@Override
public Book getBook() {
if (myBook == null) {
//根据文件路径去尝试获取book
myBook = Collection.getBookByFile(myFile.getPath());
if (myBook == null) {
myBook = NULL_BOOK;
}
}
return myBook instanceof Book ? (Book)myBook : null;
}
复制代码
这里又出现了Collection,我们知道其最终执行者是BookCollection,那我们就直接进入BookCollection的getBookByFile(path)方法看一下:
public DbBook getBookByFile(String path) {
//熟悉的方法,根据路径生成不同的文件类型,我们知道SD卡中的文件最终会生成ZLPhysicalFile
return getBookByFile(ZLFile.createFileByPath(path));
}
private DbBook getBookByFile(ZLFile bookFile) {
if (bookFile == null) {
return null;
}
return getBookByFile(bookFile, PluginCollection.getPlugin(bookFile));
}
private DbBook getBookByFile(ZLFile bookFile, final FormatPlugin plugin) {
if (plugin == null || !isFormatActive(plugin)) {
return null;
}
//忽略部分代码...
}
复制代码
至此,我们可以知道,如果plugin为空的话,那么就会直接返回null,所以plugin不为空是至关重要的,那么我们就去看一下这个plugin是怎么获取的:
public FormatPlugin getPlugin(ZLFile file) {
final FileType fileType = FileTypeCollection.Instance.typeForFile(file);
final FormatPlugin plugin = getPlugin(fileType);
if (plugin instanceof ExternalFormatPlugin) {
return file == file.getPhysicalFile() ? plugin : null;
}
return plugin;
}
复制代码
通过FileTypeCollection来获取当前file的文件类型,那么FileTypeCollection中定义了哪些可以识别的电子书文件类型呢?可以从其构造方法中看出:
private FileTypeCollection() {
//fb2
addType(new FileTypeFB2());
//epub oebzip opf
addType(new FileTypeEpub());
//mobi azw azw3
addType(new FileTypeMobipocket());
//html htm
addType(new FileTypeHtml());
//txt
addType(new SimpleFileType("txt", "txt", MimeType.TYPES_TXT));
//rtf
addType(new SimpleFileType("RTF", "rtf", MimeType.TYPES_RTF));
//pdf
addType(new SimpleFileType("PDF", "pdf", MimeType.TYPES_PDF));
//djvu djv
addType(new FileTypeDjVu());
//cbz cbr
addType(new FileTypeCBZ());
//zip
addType(new SimpleFileType("ZIP archive", "zip", Collections.singletonList(MimeType.APP_ZIP)));
//msdoc
addType(new SimpleFileType("msdoc", "doc", MimeType.TYPES_DOC));
}
复制代码
FileTypeCollection是怎么根据ZLFile获取其文件类型的呢?拿epub格式为例,其实现在FileTypeEpub类中:
@Override
public boolean acceptsFile(ZLFile file) {
//获取文件扩展名
final String extension = file.getExtension();
return //比对扩展名
"epub".equalsIgnoreCase(extension) ||
"oebzip".equalsIgnoreCase(extension) ||
("opf".equalsIgnoreCase(extension) && file != file.getPhysicalFile());
}
复制代码
很明显,是根据文件扩展名来获取文件类型,如果能获取到文件类型FileType对象,则说明当前文件为可支持的电子书文件,进而调用后面的getPlugin(fileType),就能够取到对应的文件解析插件。这个时候我们再回头去看BookCollection的getBookByFile方法:
private DbBook getBookByFile(ZLFile bookFile, final FormatPlugin plugin) {
//忽略部分代码...
book = new DbBook(bookFile, plugin);
//保存识别出来的book到数据库
saveBook(book);
return book;
}
复制代码
这里会生成DbBook,而且DbBook在生成的时候会通过插件plugin去读取Book中的内容:
DbBook(ZLFile file, FormatPlugin plugin) throws BookReadingException {
this(-1, plugin.realBookFile(file), null, null, null);
BookUtil.readMetainfo(this, plugin);
mySaveState = SaveState.NotSaved;
}
static void readMetainfo(AbstractBook book, FormatPlugin plugin) throws BookReadingException {
book.myEncoding = null;
book.myLanguage = null;
book.setTitle(null);
book.myAuthors = null;
book.myTags = null;
book.mySeriesInfo = null;
book.myUids = null;
book.mySaveState = AbstractBook.SaveState.NotSaved;
//读取book内容
plugin.readMetainfo(book);
if (book.myUids == null || book.myUids.isEmpty()) {
plugin.readUids(book);
}
//忽略部分代码...
}
复制代码
plugin读取Book内容的方法,最终会调用native方法:
private native int readMetainfoNative(AbstractBook book);
复制代码
针对这部分内容,我们来做一个总结:
- 在生成子tree时,ZLFile.createFileByPath会首先被调用,并且会根据对文件路径的判别生成对应的文件类型
- 如果路径为手机存储的路径时,会生成ZLPhysicalFile
- ZLPhysicalFile在被创建时,为根据文件路径实例化真实的File,并且会根据文件的扩展名,设置当前文件对应的archiveType
- LibraryActivity的Adapter在getView方法中,会根据CoverManager对当前tree的封面获取情况,来设置具体的图标
- CoverManager在获取当前tree的封面时,会触发Tree的getBook方法,来尝试获取当前路径对应的Book
- getBook方法会触发getPlugin方法,而getPlugin则是会判断文件扩展名,当扩展名为支持的文件类型时,才会返回对应的文件解析插件getPlugin,可支持的文件类型定义再FileTypeCollection
- 当文件类型为支持电子书格式时,最终会调用new DbBook(bookFile, plugin)生成book
- DbBook在初始化时会调用plugin的readMetainfo并最终调用native方法readMetainfoNative去读取book内容,如编码、语言、标题、作者、标签等等
五、打开电子书,终于进入了阅读页面
还记得LibraryActivity的方法:
@Override
protected void onListItemClick(ListView listView, View view, int position, long rowId) {
//忽略部分代码...
final LibraryTree tree = (LibraryTree)getTreeAdapter().getItem(position);
final Book book = tree.getBook();
if (book != null) {
//显示图书信息
showBookInfo(book);
}
}
private void showBookInfo(Book book) {
final Intent intent = new Intent(getApplicationContext(), BookInfoActivity.class);
FBReaderIntents.putBookExtra(intent, book);
OrientationUtil.startActivity(this, intent);
}
复制代码
进入图书信息Activity
废话不多说,直接看阅读按钮:
setupButton(R.id.book_info_button_open, "openBook", new View.OnClickListener() {
public void onClick(View view) {
if (myDontReloadBook) {
finish();
} else {
//阅读电子书,传递参数为book
FBReader.openBookActivity(BookInfoActivity.this, myBook, null);
}
}
});
复制代码
终于,进入了阅读页面!
当然,由于本人接触此项目时间有限,而且书写技术文章的经验实在欠缺,过程中难免会有存在错误或描述不清或语言累赘等等一些问题,还望大家能够谅解,同时也希望大家继续给予指正。最后,感谢大家对我的支持,让我有了强大的动力坚持下去。