package com.novelbook.android.utils; import android.app.ProgressDialog; import android.content.ContentValues; import android.content.Context; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; import com.google.gson.Gson; import com.novelbook.android.BookActivity; import com.novelbook.android.MyApp; import com.novelbook.android.bean.Cache; import com.novelbook.android.bean.NovelSites; import com.novelbook.android.bean.Site; import com.novelbook.android.db.SiteRule; import com.novelbook.android.db.Chapter; import com.novelbook.android.db.Novel; import com.novelbook.android.netsubscribe.BookSubscribe; import com.novelbook.android.netutils.HttpMethods; import com.novelbook.android.netutils.OnSuccessAndFaultListener; import com.novelbook.android.netutils.OnSuccessAndFaultSub; import org.json.JSONException; import org.json.JSONObject; import org.litepal.LitePal; import java.io.Console; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import okhttp3.Call; import okhttp3.Callback; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; public class BookUtil { public static final String TAG ="BookUtil"; public static final String storagePath = FileUtils.getDiskCacheDir(MyApp.applicationContext);//Environment.getExternalStorageDirectory() + "/zhuike"; public static final String cachedPath = storagePath + "/cache/"; public static final String chapterPath = storagePath + "/chapter/"; private static final String charachterType = "utf-8";//"UTF-16LE"; private Context mContext; private ProgressDialog progressDialog; private MuluStatus mMuluStatus; //目录是否下载完成 private Gson gson = new Gson(); public void setContext(Context context) { this.mContext = context; } public boolean isReadingCatalogs() { return mMuluStatus == MuluStatus.isDownloading; } //存储的字符数 public static final int cachedSize = 30000; // protected final ArrayList> myArray = new ArrayList<>(); public static final String lineBreakChar ="\n"; protected final ArrayList myArray = new ArrayList<>(); //目录 private List mChapters = new ArrayList<>(); //当前章节 // private Chapter mCurrentChapter; public List getChapters() { return mChapters; } public void setChapters(List chapters) { this.mChapters = chapters; } private String m_strCharsetName; private String bookName; private String bookPath; public void setBookLen(long bookLen) { this.bookLen = bookLen; } private long bookLen; private long position; private Novel mNovel; public void setNovel(Novel novel) { this.mNovel = novel; } public Novel getNovel( ) { return mNovel ; } //当前目录网站列表 private NovelSites mNovelSites; //当前目录网站 private Site mSite; private SiteRule mSiteRule; public void setNovelSites(NovelSites nvs) { this.mNovelSites = nvs; if(nvs.getSites().length ==0){ return; } mSite =nvs.getSites()[0]; if(nvs.getSites().length > 0) for (Site site:nvs.getSites() ) { if(site.getSelectedByDefault()){ mSite = site; break; } } getSiteRule(); } private void setSiteInfo() { File file = new File(getChapterPath() +mSite.getDomain()); if(!file.exists()){ file.mkdir(); } mNovel.setDomain(mSite.getDomain()); mNovel.setMuluUrl(mSite.getMuluUrl()); mNovel.update(mNovel.getId()); } public void getTargetSites(){ BookSubscribe.getNovelSites(mNovel.getNovelId(),new OnSuccessAndFaultSub(new OnSuccessAndFaultListener() { @Override public void onSuccess(String result) { //成功 try { NovelSites nvs = (NovelSites) gson.fromJson(result,NovelSites.class); //pageFactory.prepareBook(mNovel,nvs, BookActivity.this); setNovelSites(nvs); } catch ( Exception e) { e.printStackTrace(); } // Toast.makeText(mContext,"getMuluInfo 请求成功 " ,Toast.LENGTH_SHORT).show(); } @Override public void onFault(String errorMsg) { //失败 // Toast.makeText(mContext,"getMuluInfo 请求失败"+errorMsg,Toast.LENGTH_SHORT).show(); } },null)); } private void getSiteRule() { mSiteRule = null; BookSubscribe.getSiteRule(mSite.getDomain(),new OnSuccessAndFaultSub(new OnSuccessAndFaultListener() { @Override public void onSuccess(String result) { //成功 SiteRule sr = (SiteRule)gson.fromJson(result,SiteRule.class); List srs = LitePal.where("domain=?",sr.getDomain()).limit(1).find(SiteRule.class); long id = srs.size()==1 ?srs.get(0).getId() :0; if(id>0 ){ sr.update(id); // mSiteRule =LitePal.find(SiteRule.class,id); }else { sr.save(); } mSiteRule =sr; setSiteInfo(); Log.d(TAG, String.format("目录正则表达式下载完成,开始读取章节信息") ); readChaptersAsync(); } @Override public void onFault(String errorMsg) { //失败 Log.d(TAG,"error on get sitRule: "+errorMsg); } },mContext)); /* if(mSiteRule==null && mSite!=null) { List srs = LitePal.where("domain=?", mSite.getDomain()).find(SiteRule.class); if (srs.size() > 0) { mSiteRule = srs.get(0); } }*/ } public void setChapterNo(int chapterNo) { this.chapterNo = chapterNo; } public int getChapterNo() { return chapterNo; } private int chapterNo;//当前章节 public String getLineBreakChar(){ return "\n"; } public BookUtil(){ checkAndCreateDir(storagePath); checkAndCreateDir(chapterPath); checkAndCreateDir(cachedPath); } enum MuluStatus{ isDownloading, isDone, failed } private void showProgressDialog() { if ( null == progressDialog) { progressDialog =new ProgressDialog(mContext); } progressDialog.show(); // progressDialog.show(mContext,"网络不给力","正努力加载",false,true); } private void dismissProgressDialog() { if ( null != progressDialog) { progressDialog.dismiss(); } } private void checkAndCreateDir(String path){ File file = new File(path); if (!file.exists()){ file.mkdir(); } } public synchronized void openBook(Novel novel) throws IOException, InterruptedException { this.mNovel = novel; //如果当前缓存不是要打开的书本就缓存书本同时删除缓存 //TODO 构建新的缓存策略,几个选项,1:每本书一个缓存 2:控制缓存总大小,超过限制删除旧缓存 3:网络小说的缓存 boolean isLocalImport = novel.isLocalBook(); boolean isOnShelf = isLocalImport || novel.isOnShelf(); boolean isLoadChaptsFromRemote = !isLocalImport ;// && !novel.isFinished() ; //是否从目标网站下载目录 // showProgressDialog(); if(isLocalImport) { mChapters = LitePal.where("novelId=?", mNovel.getId() + "").find(Chapter.class); for (Chapter c : mChapters) { Log.d(TAG, String.format("bookchapter :%s,fileName :%s, chapter Size %s", c.getChapterName(), c.getChapterPath(), c.getLength())); } chaptCache = new HashMap(); if (mChapters.isEmpty()) { //1. 首次打开 本地导入的书 if (bookPath == null || !bookPath.equals(mNovel.getNovelPath())) { cleanCacheFile(); this.bookPath = mNovel.getNovelPath(); bookName = FileUtils.getFileName(bookPath); cacheBook(); } } }else{ //读取目录列表 MuluStatus m = mMuluStatus; // Log.d(TAG,String.format("mulu on Site %s download status %s",mSite.getDomain(),mMuluStatus)); while(mMuluStatus == MuluStatus.isDownloading){ Thread.sleep(50); Log.d(TAG,String.format("waiting for mulu downloading ,mMuluStatus %s" ,mMuluStatus)); if(mMuluStatus == MuluStatus.failed){ dismissProgressDialog(); throw new RuntimeException("读取资源失败,请检查网络"); } } } // dismissProgressDialog(); } // String getMuluUrl() { // return "https://www.qu.la/book/390/"; // } /* void readChapters( String url){ Request request = getTagRequest(url); ResponseBody body =null; try { long startTime= new Date().getTime(); Log.d(TAG,String.format("loadChaptContent----start download %s 目录 from %s", mNovel.getName() ,url )); Response response = HttpMethods.getOkClient().newCall(request).execute(); Log.d(TAG,String.format("loadChaptContent----end download %s 目录, 目录数量 %s, cost %s", mNovel.getName() , mChapters.size(), new Date().getTime() -startTime )); startTime= new Date().getTime(); body = response.body(); String bodyStr = body.string(); Log.d(TAG, "onResponse: " +bodyStr); buildCharacters(bodyStr,url); Log.d(TAG,String.format("loadChaptContent----end build %s 目录, 目录数量 %s, cost %s", mNovel.getName() , mChapters.size(), new Date().getTime() -startTime )); } catch (IOException e) { e.printStackTrace(); }finally { if(body!=null){ body.close();; } } }*/ void readChaptersAsync( ) { String url = mSite.getMuluUrl(); Request request = getTagRequest(url); mMuluStatus = MuluStatus.isDownloading; long startTime= new Date().getTime(); Log.d(TAG,String.format("loadChapts----start download %s 目录 from %s", mNovel.getName() ,url )); HttpMethods.getOkClient().newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { Log.d(TAG, "onFailure: " + e.getMessage()); Log.d(TAG,String.format("loadChapts---- failed %s 目录 from %s", mNovel.getName() ,url )); if( mNovelSites.getSites().length ==1){ //仅有一个rule,且失败了 mMuluStatus = MuluStatus.failed; return; } //try next site for(Site st : mNovelSites.getSites() ){ if(!st.getDomain().equals(mSite.getDomain())){ mSite =st; break; } } // readChaptersAsync(); getSiteRule(); } @Override public void onResponse(Call call, Response response){ ResponseBody body = response.body(); if(response.code()!=200){ Log.d(TAG,String.format("loadChapts----failed, %s 目录 from %s,return code %s", mNovel.getName() ,url,response.code() )); mMuluStatus = MuluStatus.failed; return; } if (body != null) { try { String bodyStr = body.string(); // Log.d(TAG, "onResponse: " +bodyStr); // Log.d(TAG,String.format("loadChaptContent----end download %s 目录, 目录数量 %s, cost %s", mNovel.getName() , mChapters.size(), new Date().getTime() -startTime )); // long startTime2= new Date().getTime(); buildCharacters(bodyStr,url); Log.d(TAG,String.format("loadChaptContent----end download %s 目录, 目录数量 %s, cost %s", mNovel.getName() , mChapters.size(), new Date().getTime() -startTime )); mMuluStatus = MuluStatus.isDone; } catch (IOException e) { e.printStackTrace(); }finally { body.close(); } } } }); } void buildCharacters( String content ,String url){ try { JSONObject siteJson = new JSONObject(); siteJson.put("chapterUrlPattern", mSiteRule.getChapterUrlPattern()); siteJson.put("chapterUrlRegexOnMulu", mSiteRule.getChapterUrlRegexOnMulu());//示例接口表达式有问题 // siteJson.put("chapterUrlRegexOnMulu", "
]*href=\"(/book/[\\d]+/[\\d]+\\.html)\">([^<]+)
"); siteJson.put("chapterUrlRegexOnMulu", mSiteRule.getChapterUrlRegexOnMulu()); mChapters = NovelParseUtil.getChapters(mSite.getDomain(),url, content, siteJson); Log.d(TAG,String.format("mulu on Site %s download status %s",mSite.getDomain(),mMuluStatus)); /* if (mChapters != null) { int lastReadChapt = mNovel.getLastReadChapt(); // int index =lastReadChapt*2-2; lastReadChapt = lastReadChapt >=mChapters.size() ? mChapters.size() -1:lastReadChapt; lastReadChapt = lastReadChapt <=0 ? 1:lastReadChapt; mCurrentChapter =mChapters.get(lastReadChapt-1); }*/ } catch (JSONException e) { // } catch (JSONException | IOException e) { Log.d(TAG,String.format("mulu on Site %s download status %s",mSite.getDomain(),mMuluStatus)); e.printStackTrace(); } finally { // result.close(); // if (result2 != null) result2.close(); } } private void cleanCacheFile(){ File file = new File(cachedPath ); if (!file.exists()){ file.mkdir(); }else{ File[] files = file.listFiles(); for (int i = 0; i < files.length;i++){ files[i].delete(); } } file = new File(getChapterPath()); if (!file.exists()){ file.mkdir(); }else{ File[] files = file.listFiles(); for (int i = 0; i < files.length;i++){ files[i].delete(); } } } public int next(boolean back){ position += 1; if (position > bookLen){ position = bookLen; return -1; } char result = chaptCurrent(); //current(); if (back) { position -= 1; } return result; } public char[] nextLine(){ if (position >= bookLen){ return null; } String line = ""; while (position < bookLen){ int word = next(false); if (word == -1){ break; } char wordChar = (char) word; if ((wordChar + "").equals("\n") ){// if ((wordChar + "").equals("\r") && (((char)next(true)) + "").equals("\n")){ // next(false); break; } line += wordChar; } return line.toCharArray(); } public char[] preLine(){ if (position <= 0){ return null; } String line = ""; while (position >= 0){ int word = pre(false); if (word == -1){ break; } char wordChar = (char) word; if ((wordChar + "").equals("\n") ){ // if ((wordChar + "").equals("\n") && (((char)pre(true)) + "").equals("\r")){ // pre(false); // /r/n ->/n 不需要再往前读一个字符了 // line = "\r\n" + line; break; } line = wordChar + line; } return line.toCharArray(); } public char chaptCurrent(){ char[] charArray = chaptChars(chapterNo); return charArray[(int)position-1]; } public char current(){ // int pos = (int) (position % cachedSize); // int cachePos = (int) (position / cachedSize); int cachePos = 0; int pos = 0; int len = 0; for (int i = 0;i < myArray.size();i++){ long size = myArray.get(i).getSize(); if (size + len - 1 >= position){ cachePos = i; pos = (int) (position - len); break; } len += size; } char[] charArray = block(cachePos); return charArray[pos]; } public int pre(boolean back){ position -= 1; if (position < 0){ position = 0; return -1; } char result = current(); if (back) { position += 1; } return result; } public long getPosition(){ return position; } public void setPostition(long position){ this.position = position; } //缓存书本 private void cacheBook() throws IOException { if (TextUtils.isEmpty(mNovel.getCharset())) { m_strCharsetName = FileUtils.getCharset(bookPath); if (m_strCharsetName == null) { m_strCharsetName = "utf-8"; } ContentValues values = new ContentValues(); values.put("charset",m_strCharsetName); LitePal.update(Novel.class,values,mNovel.getId()); }else{ m_strCharsetName = mNovel.getCharset(); } File file = new File(bookPath); InputStreamReader reader = new InputStreamReader(new FileInputStream(file),m_strCharsetName); int index = 0; bookLen = 0; mChapters.clear(); myArray.clear(); while (true){ char[] buf = new char[cachedSize]; int result = reader.read(buf); if (result == -1){ reader.close(); break; } String bufStr = new String(buf); // Log.e(TAG,String.format("缓存的内容是\n %s",bufStr)); bufStr = bufStr.replaceAll("\r\n","\n"); // bufStr = bufStr.replaceAll("\u3000\u3000+[ ]*","\u3000\u3000"); bufStr = bufStr.replaceAll("\n+\\s*","\n\u3000\u3000");// bufStr = bufStr.replaceAll("\r\n+\\s*","\r\n\u3000\u3000"); // bufStr = bufStr.replaceAll("\r\n[ {0,}]","\r\n\u3000\u3000"); // bufStr = bufStr.replaceAll(" ",""); bufStr = bufStr.replaceAll("\u0000",""); buf = bufStr.toCharArray(); bookLen += buf.length; // Log.e(TAG,String.format("缓存的内容脱空格处理后\n %s",bufStr)); Cache cache = new Cache(); cache.setSize(buf.length); cache.setData(new WeakReference(buf)); // bookLen += result; myArray.add(cache); // myArray.add(new WeakReference(buf)); // myArray.set(index,); Log.e(TAG,String.format("缓存的内容写入文件\n %s",fileName(index))); Log.e(TAG,"---------------------------------------------------------------------------------------------------------"); try { File cacheBook = new File(fileName(index)); if (!cacheBook.exists()){ cacheBook.createNewFile(); } final OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(fileName(index)), "UTF-16LE"); // UTF-16LE 比 utf-8 文件小 writer.write(buf); writer.close(); } catch (IOException e) { throw new RuntimeException("Error during writing " + fileName(index)); } index ++; } chaptId =0; //初始化导入的chapid int endchp = myArray.size()>3 ?3:myArray.size(); getChapter(1,3); //先导入2个部分 立即进行阅读 new Thread(){ @Override public void run() { getChapter(4,myArray.size()); //剩余部分后台导入 } }.start(); } int chaptId =0; //获取章节 public synchronized void getChapter(int startblk,int endblk){ if(endblk getmChapters(){ return mChapters; } public long getBookLen(){ return bookLen; } protected String fileName(int index) { return cachedPath + mNovel.getName() + index ; } protected String fileChapterName(int chaptId ) { if(!TextUtils.isEmpty(mNovel.getDomain())){ return getChapterPath() +mNovel.getDomain()+"/"+ chaptId ; } return getChapterPath() + chaptId ; } String getChapterPath(){ File file = new File(chapterPath +mNovel.getId()); if(!file.exists()){ file.mkdir(); } return chapterPath +mNovel.getId()+"/"; } //获取书本缓存 public char[] block(int index) { if (myArray.size() == 0){ return new char[1]; } char[] block = myArray.get(index).getData().get(); if (block == null) { try { File file = new File(fileName(index)); int size = (int)file.length(); if (size < 0) { throw new RuntimeException("Error during reading " + fileName(index)); } block = new char[size / 2]; InputStreamReader reader = new InputStreamReader( new FileInputStream(file), "UTF-16LE" ); if (reader.read(block) != block.length) { throw new RuntimeException("Error during reading " + fileName(index)); } reader.close(); } catch (IOException e) { throw new RuntimeException("Error during reading " + fileName(index)); } Cache cache = myArray.get(index); cache.setData(new WeakReference(block)); // myArray.set(index, new WeakReference(block)); } return block; } boolean isDownloadChapt =false; synchronized boolean getDownloadStatus(){ return isDownloadChapt; } synchronized void setDownloadFlag(boolean flag){ isDownloadChapt = flag; Log.d("loadChaptContent",String.format("set download flat",isDownloadChapt) ); } final Handler handler = new Handler() { @Override public void handleMessage(Message msg) { int wt = msg.what; dismissProgressDialog(); if (msg.what == 123) { isDownloadChapt =true; Log.d("loadChaptContent",String.format("handler msg, download %s",isDownloadChapt) ); }else if(msg.what==1){ isDownloadChapt =true; // Toast.makeText(mContext,"网络错误",Toast.LENGTH_LONG).show(); } } }; private Map chaptCache = new HashMap(); private Map chaptDownStatus = new HashMap(); DownloadStatus downloadStatus = DownloadStatus.notStart; private enum DownloadStatus{ notStart, downloading, failure, success } //获取chapter 缓存 public char[] chaptChars(int index) { char[] block=null; if(chaptCache.containsKey(Integer.valueOf(index))) { block = chaptCache .get(index).getData().get(); } if (block == null) { // cleanCacheFile(); //to remove try { File file = new File(fileChapterName(index)); if(!file.exists()) { int slept = 0; while(slept <100 && mMuluStatus ==MuluStatus.isDownloading){ try { Thread.sleep(50); slept++; } catch (InterruptedException e) { e.printStackTrace(); } } if( mChapters ==null || mChapters.size() ==0){ String error = "获取目录失败,网络错误,请重试"; return error.toCharArray(); } Log.d(TAG,String.format("loadChaptContent----start %s" ,new Date().toString() )); //showProgressDialog();//why not show Log.d( "loadChaptContent",String.format("begin to load content for chapter %s",index)); Log.d( "loadChaptContent",String.format("isDownloadChapt: %s",isDownloadChapt)); if(!chaptDownStatus.containsKey(Integer.valueOf(index))){ chaptDownStatus.put(index,DownloadStatus.downloading); loadChaptContent(index); } Log.d( "loadChaptContent",String.format("showing dialog " )); // Log.d(TAG,String.format("showing progress diaglog......")); int maxSleep =6000; int slepttime =0; // while(!file.exists() && !getDownloadStatus()){//&& slepttime index ) { File file2 = new File(fileChapterName(index+1)); if(!file2.exists()) { loadChaptContent(index + 1); } } int size = (int)file.length(); if (size < 0) { throw new RuntimeException("Error during reading " + fileChapterName(index)); } block = new char[size / 2]; InputStreamReader reader = new InputStreamReader( new FileInputStream(file), charachterType ); long l = reader.read(block); if (reader.read(block) != block.length) { // throw new RuntimeException("Error during reading " + fileChapterName(index)); } reader.close(); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException("Error during reading " + fileChapterName(index)); } catch (JSONException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); }finally { dismissProgressDialog(); } Cache cache = new Cache(); cache.setSize(block.length); cache.setData(new WeakReference(block)); chaptCache.put(index, cache); // myArray.set(index, new WeakReference(block)); } return block; } private void loadChaptContent(int index) throws JSONException, InterruptedException { /* 章节内容没有缓存在本地 1. 根据本地的章节网络地址信息,读取章节内容到本地,若读取失败则 2. 查询主服务器,若有地址更新则更新本地信息,并重复1,若没有更新地址,则地址无效,返回章节内容正待手打 */ // Chapter chapter = mChapters.get(index -1); String url = chapter.getChapterUrl(); if( TextUtils.isEmpty( url)){ return ; } long startTime= new Date().getTime(); Log.d(TAG,String.format("loadChaptContent----start download %s from %s", chapter.getChapterName() ,url )); setDownloadFlag(false); Log.d( "loadChaptContent",String.format("loadChaptContent isDownloadChapt: %s",isDownloadChapt)); JSONObject siteJson = new JSONObject(); siteJson.put("chapterContentRegex", mSiteRule.getChapterContentRegex()); siteJson.put("chapterContentDumpRegex", mSiteRule.getChapterContentDumpRegex()); Request request = getTagRequest(url); HttpMethods.getOkClient().newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { handler.sendEmptyMessage(123); handler.sendEmptyMessage(1); chaptDownStatus.put(index,DownloadStatus.failure); setDownloadFlag(true); Log.d( "loadChaptContent",String.format("loadChaptContent fail, isDownloadChapt: %s",isDownloadChapt)); e.printStackTrace(); // throw new RuntimeException("Error during writing " + fileChapterName( index)); } @Override public void onResponse(Call call, Response response){ ResponseBody body = response.body(); if (body != null ) { if(response.code()!=200){ Log.d(TAG, "loadChaptContent----network failure returnCode " + response.code()); setDownloadFlag(true); chaptDownStatus.put(index,DownloadStatus.failure); Log.d( "loadChaptContent",String.format("loadChaptContent error %s ,isDownloadChapt: %s", response.code(),isDownloadChapt)); handler.sendEmptyMessage(1); return; } try { String bodyStr = body.string(); String title = chapter.getChapterName(); String chapterContent = title+ "\n" + NovelParseUtil.getChapterContent(bodyStr, siteJson); char[] buf = chapterContent.toCharArray(); File file = new File(fileChapterName(index)); file.createNewFile(); final OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(fileChapterName(index)), charachterType);//"UTF-16LE"); // UTF-16LE 比 utf-8 文件小 writer.write(buf); writer.close(); Log.d( "loadChaptContent",String.format("loadChaptContent file created: %s", file.getPath())); handler.sendEmptyMessage(123); setDownloadFlag(true); } catch (IOException | JSONException e) { e.printStackTrace(); throw new RuntimeException("Error during writing " + fileChapterName( index)); } finally { body.close(); handler.sendEmptyMessage(123); setDownloadFlag(true); } chapter.setNovelId(mNovel.getId()); chapter.setChapterPath(fileChapterName(index)); chapter.save(); setDownloadFlag(true); chaptDownStatus.put(index,DownloadStatus.success); Log.d(TAG,String.format("loadChaptContent---- finished download %s, cost time %s ,content path %s ", chapter.getChapterName(), new Date().getTime() -startTime ,chapter.getChapterPath() )); } } }); } private Request getTagRequest(String url) { return new Request.Builder() .tag(mNovel.getNovelId()) //标记 请求的tag,切换小说或离开小说界面(BookActivity) 时 取消未执行完毕的 此tag的所有请求 .url(url) // .header("User-Agent", "OkHttp Example") .build(); } public boolean isChapterTitle(String line){ return (line.length() <= 30 && (line.matches(".*第.{1,8}章.*") || line.matches(".*第.{1,8}节.*"))) ; } }