1 package org.coolreader.crengine; 2 3 import android.annotation.SuppressLint; 4 5 import org.coolreader.CoolReader; 6 import org.coolreader.crengine.Engine.DelayedProgress; 7 import org.xml.sax.Attributes; 8 import org.xml.sax.SAXException; 9 import org.xml.sax.helpers.DefaultHandler; 10 11 import java.io.ByteArrayInputStream; 12 import java.io.File; 13 import java.io.FileOutputStream; 14 import java.io.IOException; 15 import java.io.InputStream; 16 import java.net.Authenticator; 17 import java.net.HttpURLConnection; 18 import java.net.InetSocketAddress; 19 import java.net.PasswordAuthentication; 20 import java.net.Proxy; 21 import java.net.URL; 22 import java.net.URLConnection; 23 import java.security.cert.X509Certificate; 24 import java.text.ParseException; 25 import java.text.SimpleDateFormat; 26 import java.util.ArrayList; 27 import java.util.Collection; 28 import java.util.HashSet; 29 import java.util.Stack; 30 import java.util.concurrent.Callable; 31 32 import javax.net.ssl.HttpsURLConnection; 33 import javax.net.ssl.SSLContext; 34 import javax.net.ssl.TrustManager; 35 import javax.net.ssl.X509TrustManager; 36 import javax.xml.parsers.SAXParser; 37 import javax.xml.parsers.SAXParserFactory; 38 39 @SuppressLint("SimpleDateFormat") 40 public class OPDSUtil { 41 42 public static final boolean EXTENDED_LOG = false; // set to false for production 43 public static final int CONNECT_TIMEOUT = 60000; 44 public static final int READ_TIMEOUT = 60000; 45 /* 46 <?xml version="1.0" encoding="utf-8"?> 47 <feed xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/" xmlns:relevance="http://a9.com/-/opensearch/extensions/relevance/1.0/" 48 xmlns="http://www.w3.org/2005/Atom" 49 xml:base="http://lib.ololo.cc/opds/"> 50 <id>http://lib.ololo.cc/opds/</id> 51 <updated>2011-05-31T10:28:22+04:00</updated> 52 <title>OPDS: lib.ololo.cc</title> 53 <subtitle>Librusec mirror.</subtitle> 54 <author> 55 <name>ololo team</name> 56 <uri>http://lib.ololo.cc</uri><email>libololo@gmail.com</email> 57 </author> 58 <icon>http://lib.ololo.cc/book.png</icon> 59 <link rel="self" title="This Page" type="application/atom+xml" href="/opds/"/> 60 <link rel="alternate" type="text/html" title="HTML Page" href="/"/> 61 <entry> 62 <updated>2011-05-31T10:28:22+04:00</updated> 63 <id>http://lib.ololo.cc/opds/asearch/</id> 64 <title>sample type</title> 65 <content type="text">sample content</content> 66 <link type="application/atom+xml" href="http://lib.ololo.cc/opds/asearch/"/> 67 </entry> 68 </feed> 69 */ 70 /** 71 * Callback interface for OPDS. 72 */ 73 public interface DownloadCallback { 74 /** 75 * Some entries are downloaded. 76 * @param doc is document 77 * @param entries is list of entries to add 78 */ onEntries( DocInfo doc, Collection<EntryInfo> entries )79 public boolean onEntries( DocInfo doc, Collection<EntryInfo> entries ); 80 /** 81 * All entries are downloaded. 82 * @param doc is document 83 * @param entries is list of entries to add 84 */ onFinish( DocInfo doc, Collection<EntryInfo> entries )85 public boolean onFinish( DocInfo doc, Collection<EntryInfo> entries ); 86 /** 87 * Before download: request filename to save as. 88 */ onDownloadStart( String type, String url )89 public File onDownloadStart( String type, String url ); 90 /** 91 * Download progress 92 */ onDownloadProgress( String type, String url, int percent )93 public void onDownloadProgress( String type, String url, int percent ); 94 /** 95 * Book is downloaded. 96 */ onDownloadEnd( String type, String url, File file )97 public void onDownloadEnd( String type, String url, File file ); 98 /** 99 * Error occured 100 */ onError( String message )101 public void onError( String message ); 102 } 103 104 public static class DocInfo { 105 public String id; 106 public long updated; 107 public String title; 108 public String subtitle; 109 public String icon; 110 public String language; 111 public LinkInfo selfLink; 112 public LinkInfo alternateLink; 113 public LinkInfo nextLink; 114 } 115 dirPath(String filePath)116 public static String dirPath(String filePath) { 117 int pos = filePath.lastIndexOf("/"); 118 if (pos < 0) 119 return filePath; 120 return filePath.substring(0, pos+1); 121 } 122 123 public static class LinkInfo { 124 public String href; 125 public String rel; 126 public String title; 127 public String type; LinkInfo( URL baseURL, Attributes attributes )128 public LinkInfo( URL baseURL, Attributes attributes ) { 129 rel = attributes.getValue("rel"); 130 type = attributes.getValue("type"); 131 title = attributes.getValue("title"); 132 href = convertHref( baseURL, attributes.getValue("href") ); 133 } convertHref( URL baseURL, String href )134 public static String convertHref( URL baseURL, String href ) { 135 if ( href==null ) 136 return href; 137 String port = ""; 138 if (baseURL.getPort() != 80 && baseURL.getPort() > 0) 139 port = ":" + baseURL.getPort(); 140 String hostPort = baseURL.getHost() + port; 141 if ( href.startsWith("//") ) 142 return baseURL.getProtocol() + ":" + href; 143 if ( href.startsWith("/") ) 144 return baseURL.getProtocol() + "://" + hostPort + href; 145 if ( !href.startsWith("http://") && !href.startsWith("https://") ) { 146 return baseURL.getProtocol() + "://" + hostPort + dirPath(baseURL.getPath()) + "/" + href; 147 } 148 return href; 149 } isValid()150 public boolean isValid() { 151 return href!=null && href.length()!=0; 152 } getPriority()153 public int getPriority() { 154 if ( type==null ) 155 return 0; 156 DocumentFormat df = DocumentFormat.byMimeType(type); 157 if ( rel!=null && rel.indexOf("acquisition")<0 && df!=DocumentFormat.FB2 && df!=DocumentFormat.EPUB 158 && df!=DocumentFormat.RTF && df!=DocumentFormat.DOC) 159 return 0; 160 return df!=null ? df.getPriority() : 0; 161 } 162 @Override toString()163 public String toString() { 164 return "[ rel=" + rel + ", type=" + type 165 + ", title=" + title + ", href=" + href + "]"; 166 } 167 168 } 169 170 public static class AuthorInfo { 171 public String name; 172 public String uri; 173 } 174 175 public static class EntryInfo { 176 public String id; 177 public long updated; 178 public String title=""; 179 public String content=""; 180 public String summary=""; 181 public LinkInfo link; 182 public ArrayList<LinkInfo> links = new ArrayList<LinkInfo>(); 183 public String icon; 184 public ArrayList<String> categories = new ArrayList<String>(); 185 public ArrayList<AuthorInfo> authors = new ArrayList<AuthorInfo>(); getBestAcquisitionLink()186 public LinkInfo getBestAcquisitionLink() { 187 LinkInfo best = null; 188 int bestPriority = 0; 189 for ( LinkInfo link : links ) { 190 //boolean isAcquisition = link.rel!=null && link.rel.indexOf("acquisition")>=0; 191 int priority = link.getPriority(); 192 if (priority>0 && priority>bestPriority) { 193 if ( link.getPriority()>0 && (best==null || best.getPriority()<link.getPriority()) ) { 194 best = link; 195 bestPriority = priority; 196 } 197 } 198 } 199 return best; 200 } getAuthors()201 public String getAuthors() { 202 if ( authors.size()==0 ) 203 return null; 204 StringBuilder buf = new StringBuilder(100); 205 for ( AuthorInfo a : authors ) { 206 if ( buf.length()>0 ) 207 buf.append(", "); 208 buf.append(a.name); 209 } 210 return buf.toString(); 211 } 212 } 213 214 public static class OPDSHandler extends DefaultHandler { 215 private URL url; 216 private DocInfo docInfo = new DocInfo(); 217 private EntryInfo entryInfo = new EntryInfo(); 218 private ArrayList<EntryInfo> entries = new ArrayList<EntryInfo>(); 219 private Stack<String> elements = new Stack<String>(); 220 //private Attributes currentAttributes; 221 private AuthorInfo authorInfo; 222 private boolean insideFeed; 223 private boolean insideEntry; 224 private boolean insideEntryTitle; 225 //private boolean singleEntry; 226 private int level = 0; 227 //2011-05-31T10:28:22+04:00 228 private static SimpleDateFormat tsFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); 229 private static SimpleDateFormat tsFormat2 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); OPDSHandler( URL url )230 public OPDSHandler( URL url ) { 231 this.url = url; 232 } setUrl(URL url)233 public void setUrl(URL url) { 234 this.url = url; 235 } parseTimestamp( String ts )236 private long parseTimestamp( String ts ) { 237 if ( ts==null ) 238 return 0; 239 ts = ts.trim(); 240 try { 241 if ( ts.length()=="2010-01-10T10:01:10Z".length() ) 242 return tsFormat2.parse(ts).getTime(); 243 if ( ts.length()=="2011-11-11T11:11:11+67:87".length()&& ts.lastIndexOf(":")==ts.length()-3 ) { 244 ts = ts.substring(0, ts.length()-3) + ts.substring(0, ts.length()-2); 245 return tsFormat.parse(ts).getTime(); 246 } 247 if ( ts.length()=="2011-11-11T11:11:11+6787".length()) { 248 return tsFormat.parse(ts).getTime(); 249 } 250 } catch (ParseException e) { 251 } 252 L.e("cannot parse timestamp " + ts); 253 return 0; 254 } 255 256 @Override characters(char[] ch, int start, int length)257 public void characters(char[] ch, int start, int length) 258 throws SAXException { 259 super.characters(ch, start, length); 260 261 String s = new String( ch, start, length); 262 s = s.trim(); 263 if (s.length()==0 || (s.length()==1 && s.charAt(0) == '\n') ) 264 return; // ignore empty line 265 //L.d(tab() + " {" + s + "}"); 266 String currentElement = elements.peek(); 267 if ( currentElement==null ) 268 return; 269 if ( insideFeed ) { 270 if ( "id".equals(currentElement) ) { 271 if ( insideEntry ) 272 entryInfo.id = s; 273 else 274 docInfo.id = s; 275 } else if ( "updated".equals(currentElement) ) { 276 long ts = parseTimestamp(s); 277 if ( insideEntry ) 278 entryInfo.updated = ts; 279 else 280 docInfo.updated = ts; 281 } else if ( "title".equals(currentElement) ) { 282 if ( !insideEntry ) { 283 docInfo.title = s; 284 } else { 285 entryInfo.title = entryInfo.title + s; 286 } 287 } else if ( "summary".equals(currentElement) ) { 288 if ( insideEntry ) 289 entryInfo.summary = entryInfo.summary + s; 290 } else if ( "name".equals(currentElement) ) { 291 if ( authorInfo != null ) 292 authorInfo.name = s; 293 } else if ( "uri".equals(currentElement) ) { 294 if ( authorInfo!=null ) 295 authorInfo.uri = s; 296 } else if ( "icon".equals(currentElement) ) { 297 if ( !insideEntry ) 298 docInfo.icon = s; 299 else 300 entryInfo.icon = s; 301 } else if ( "link".equals(currentElement) ) { 302 // rel, type, title, href 303 // if ( !insideEntry ) 304 // docInfo.icon = s; 305 // else 306 // entryInfo.icon = s; 307 } else if ( "content".equals(currentElement) ) { 308 if ( insideEntry ) 309 entryInfo.content = entryInfo.content + s; 310 } else if ( "subtitle".equals(currentElement) ) { 311 if ( !insideEntry ) 312 docInfo.subtitle = s; 313 } else if ( "language".equals(currentElement) ) { 314 if ( !insideEntry ) 315 docInfo.language = s; 316 } else if ( insideEntryTitle ) { 317 if (entryInfo.title.length() > 0) 318 entryInfo.title = entryInfo.title + " "; 319 entryInfo.title = entryInfo.title + s; 320 } 321 } 322 } 323 324 @Override endDocument()325 public void endDocument() throws SAXException { 326 super.endDocument(); 327 if (EXTENDED_LOG) L.d("endDocument: " + entries.size() + " entries parsed"); 328 if (EXTENDED_LOG) 329 for ( EntryInfo entry : entries ) { 330 L.d(" " + entry.title + " : " + entry.link.toString()); 331 } 332 } 333 tab()334 private String tab() { 335 if ( level<=1 ) 336 return ""; 337 StringBuffer buf = new StringBuffer(level*2); 338 for ( int i=1; i<level; i++ ) 339 buf.append(" "); 340 return buf.toString(); 341 } 342 343 @Override startElement(String uri, String localName, String qName, Attributes attributes)344 public void startElement(String uri, String localName, 345 String qName, Attributes attributes) 346 throws SAXException { 347 super.startElement(uri, localName, qName, attributes); 348 if ( qName!=null && qName.length()>0 ) 349 localName = qName; 350 level++; 351 //L.d(tab() + "<" + localName + ">"); 352 //currentAttributes = attributes; 353 elements.push(localName); 354 //String currentElement = elements.peek(); 355 if ( !insideFeed && "feed".equals(localName) ) { 356 insideFeed = true; 357 } else if ( "entry".equals(localName) ) { 358 if ( !insideFeed ) { 359 insideFeed = true; 360 //singleEntry = true; 361 } 362 insideEntry = true; 363 entryInfo = new EntryInfo(); 364 } else if ( "category".equals(localName) ) { 365 if ( insideEntry ) { 366 String category = attributes.getValue("label"); 367 if ( category!=null ) 368 entryInfo.categories.add(category); 369 } 370 } else if ( "id".equals(localName) ) { 371 372 } else if ( "updated".equals(localName) ) { 373 374 } else if ( "title".equals(localName) ) { 375 insideEntryTitle = insideEntry; 376 } else if ( "name".equals(localName) ) { 377 378 } else if ( "link".equals(localName) ) { 379 LinkInfo link = new LinkInfo(url, attributes); 380 if ( link.isValid() && insideFeed ) { 381 if (EXTENDED_LOG) L.d(tab()+link.toString()); 382 if ( insideEntry ) { 383 if ( link.type!=null ) { 384 entryInfo.links.add(link); 385 int priority = link.getPriority(); 386 if ( link.type.startsWith("application/atom+xml") ) { 387 if (entryInfo.link == null || !entryInfo.link.type.startsWith("application/atom+xml")) 388 entryInfo.link = link; 389 } else if ( "http://opds-spec.org/cover".equals(link.rel) && "image/jpeg".equals(link.type)) { 390 entryInfo.icon = link.href; 391 } else if ( "http://opds-spec.org/thumbnail".equals(link.rel) && "image/jpeg".equals(link.type)) { 392 if (entryInfo.icon == null) 393 entryInfo.icon = link.href; 394 } else if (priority>0 && (entryInfo.link==null || entryInfo.link.getPriority()<priority)) { 395 entryInfo.link = link; 396 } 397 } 398 } else { 399 if ( "self".equals(link.rel) ) 400 docInfo.selfLink = link; 401 else if ( "alternate".equals(link.rel) ) 402 docInfo.alternateLink = link; 403 else if ( "next".equals(link.rel) ) 404 docInfo.nextLink = link; 405 } 406 } 407 } else if ( "author".equals(localName) ) { 408 authorInfo = new AuthorInfo(); 409 } 410 } 411 412 @Override endElement(String uri, String localName, String qName)413 public void endElement(String uri, String localName, 414 String qName) throws SAXException { 415 super.endElement(uri, localName, qName); 416 if ( qName!=null && qName.length()>0 ) 417 localName = qName; 418 //L.d(tab() + "</" + localName + ">"); 419 //String currentElement = elements.peek(); 420 if ( insideFeed && "feed".equals(localName) ) { 421 insideFeed = false; 422 } else if ( "title".equals(localName) ) { 423 insideEntryTitle = false; 424 } else if ( "entry".equals(localName) ) { 425 if ( !insideFeed || !insideEntry ) 426 throw new SAXException("unexpected element " + localName); 427 if ( entryInfo.link!=null || entryInfo.getBestAcquisitionLink()!=null ) { 428 entries.add(entryInfo); 429 } 430 insideEntry = false; 431 entryInfo = null; 432 } else if ( "author".equals(localName) ) { 433 if (insideEntry) { 434 if ( authorInfo!=null && authorInfo.name!=null ) 435 entryInfo.authors.add(authorInfo); 436 } 437 authorInfo = null; 438 } 439 //currentAttributes = null; 440 if ( level>0 ) 441 level--; 442 } 443 444 @Override startDocument()445 public void startDocument() throws SAXException { 446 // TODO Auto-generated method stub 447 super.startDocument(); 448 } 449 450 } 451 452 public static class DownloadTask { 453 final private CoolReader coolReader; 454 private URL url; 455 private String username; 456 private String password; 457 final private String expectedType; 458 final private String referer; 459 final private String defaultFileName; 460 final private DownloadCallback callback; 461 private String progressMessage = "Dowloading..."; 462 private HttpURLConnection connection; 463 private DelayedProgress delayedProgress; 464 OPDSHandler handler; DownloadTask(CoolReader coolReader, URL url, String defaultFileName, String expectedType, String referer, DownloadCallback callback, String username, String password)465 public DownloadTask(CoolReader coolReader, URL url, String defaultFileName, String expectedType, String referer, DownloadCallback callback, String username, String password) { 466 this.url = url; 467 this.coolReader = coolReader; 468 this.callback = callback; 469 this.referer = referer; 470 this.expectedType = expectedType; 471 this.defaultFileName = defaultFileName; 472 this.username = username; 473 this.password = password; 474 L.d("Created DownloadTask for " + url); 475 } setProgressMessage( String url, int totalSize )476 private void setProgressMessage( String url, int totalSize ) { 477 progressMessage = coolReader.getString(org.coolreader.R.string.progress_downloading) + " " + url; 478 if ( totalSize>0 ) 479 progressMessage = progressMessage + " (" + totalSize + ")"; 480 } 481 // call in GUI thread only! hideProgress()482 private void hideProgress() { 483 if ( progressShown && Services.getEngine() != null) 484 Services.getEngine().hideProgress(); 485 if ( delayedProgress != null ) { 486 delayedProgress.cancel(); 487 delayedProgress.hide(); 488 } 489 } onError(final String msg)490 private void onError(final String msg) { 491 BackgroundThread.instance().executeGUI(() -> { 492 hideProgress(); 493 callback.onError(msg); 494 }); 495 } parseFeed( InputStream is )496 private void parseFeed( InputStream is ) throws Exception { 497 try { 498 if (handler==null) 499 handler = new OPDSHandler(url); 500 else 501 handler.setUrl(url); // download next part 502 String[] namespaces = new String[] { 503 "access", "http://www.bloglines.com/about/specs/fac-1.0", 504 "admin", "http://webns.net/mvcb/", 505 "ag", "http://purl.org/rss/1.0/modules/aggregation/", 506 "annotate", "http://purl.org/rss/1.0/modules/annotate/", 507 "app", "http://www.w3.org/2007/app", 508 "atom", "http://www.w3.org/2005/Atom", 509 "audio", "http://media.tangent.org/rss/1.0/", 510 "blogChannel", "http://backend.userland.com/blogChannelModule", 511 "cc", "http://web.resource.org/cc/", 512 "cf", "http://www.microsoft.com/schemas/rss/core/2005", 513 "company", "http://purl.org/rss/1.0/modules/company", 514 "content", "http://purl.org/rss/1.0/modules/content/", 515 "conversationsNetwork", "http://conversationsnetwork.org/rssNamespace-1.0/", 516 "cp", "http://my.theinfo.org/changed/1.0/rss/", 517 "creativeCommons", "http://backend.userland.com/creativeCommonsRssModule", 518 "dc", "http://purl.org/dc/elements/1.1/", 519 "dcterms", "http://purl.org/dc/terms/", 520 "email", "http://purl.org/rss/1.0/modules/email/", 521 "ev", "http://purl.org/rss/1.0/modules/event/", 522 "feedburner", "http://rssnamespace.org/feedburner/ext/1.0", 523 "fh", "http://purl.org/syndication/history/1.0", 524 "foaf", "http://xmlns.com/foaf/0.1/", 525 "foaf", "http://xmlns.com/foaf/0.1", 526 "geo", "http://www.w3.org/2003/01/geo/wgs84_pos#", 527 "georss", "http://www.georss.org/georss", 528 "geourl", "http://geourl.org/rss/module/", 529 "g", "http://base.google.com/ns/1.0", 530 "gml", "http://www.opengis.net/gml", 531 "icbm", "http://postneo.com/icbm", 532 "image", "http://purl.org/rss/1.0/modules/image/", 533 "indexing", "urn:atom-extension:indexing", 534 "itunes", "http://www.itunes.com/dtds/podcast-1.0.dtd", 535 "kml20", "http://earth.google.com/kml/2.0", 536 "kml21", "http://earth.google.com/kml/2.1", 537 "kml22", "http://www.opengis.net/kml/2.2", 538 "l", "http://purl.org/rss/1.0/modules/link/", 539 "mathml", "http://www.w3.org/1998/Math/MathML", 540 "media", "http://search.yahoo.com/mrss/", 541 "openid", "http://openid.net/xmlns/1.0", 542 "opensearch10", "http://a9.com/-/spec/opensearchrss/1.0/", 543 "opensearch", "http://a9.com/-/spec/opensearch/1.1/", 544 "opml", "http://www.opml.org/spec2", 545 "rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#", 546 "rdfs", "http://www.w3.org/2000/01/rdf-schema#", 547 "ref", "http://purl.org/rss/1.0/modules/reference/", 548 "reqv", "http://purl.org/rss/1.0/modules/richequiv/", 549 "rss090", "http://my.netscape.com/rdf/simple/0.9/", 550 "rss091", "http://purl.org/rss/1.0/modules/rss091#", 551 "rss1", "http://purl.org/rss/1.0/", 552 "rss11", "http://purl.org/net/rss1.1#", 553 "search", "http://purl.org/rss/1.0/modules/search/", 554 "slash", "http://purl.org/rss/1.0/modules/slash/", 555 "ss", "http://purl.org/rss/1.0/modules/servicestatus/", 556 "str", "http://hacks.benhammersley.com/rss/streaming/", 557 "sub", "http://purl.org/rss/1.0/modules/subscription/", 558 "svg", "http://www.w3.org/2000/svg", 559 "sx", "http://feedsync.org/2007/feedsync", 560 "sy", "http://purl.org/rss/1.0/modules/syndication/", 561 "taxo", "http://purl.org/rss/1.0/modules/taxonomy/", 562 "thr", "http://purl.org/rss/1.0/modules/threading/", 563 "thr", "http://purl.org/syndication/thread/1.0", 564 "trackback", "http://madskills.com/public/xml/rss/module/trackback/", 565 "wfw", "http://wellformedweb.org/CommentAPI/", 566 "wiki", "http://purl.org/rss/1.0/modules/wiki/", 567 "xhtml", "http://www.w3.org/1999/xhtml", 568 "xlink", "http://www.w3.org/1999/xlink", 569 "xrd", "xri://$xrd*($v*2.0)", 570 "xrds", "xri://$xrds" 571 }; 572 for ( int i=0; i<namespaces.length-1; i+=2 ) 573 handler.startPrefixMapping(namespaces[i], namespaces[i+1]); 574 SAXParserFactory spf = SAXParserFactory.newInstance(); 575 spf.setValidating(false); 576 // spf.setNamespaceAware(true); 577 // spf.setFeature("http://xml.org/sax/features/namespaces", false); 578 SAXParser sp = spf.newSAXParser(); 579 //XMLReader xr = sp.getXMLReader(); 580 sp.parse(is, handler); 581 } catch (SAXException se) { 582 L.e("sax error", se); 583 throw se; 584 } catch (IOException ioe) { 585 L.e("sax parse io error", ioe); 586 throw ioe; 587 } 588 } 589 generateFileName( File outDir, String fileName, String type, boolean isZip )590 private File generateFileName( File outDir, String fileName, String type, boolean isZip ) { 591 DocumentFormat fmt = type!=null ? DocumentFormat.byMimeType(type) : null; 592 //DocumentFormat fmtext = fileName!=null ? DocumentFormat.byExtension(fileName) : null; 593 if ( fileName==null ) 594 fileName = "noname"; 595 String ext = null; 596 if ( fileName.lastIndexOf(".")>0 ) { 597 ext = fileName.substring(fileName.lastIndexOf(".")+1); 598 fileName = fileName.substring(0, fileName.lastIndexOf(".")); 599 } 600 fileName = Utils.transcribeFileName( fileName ); 601 if ( fmt!=null ) { 602 if ( fmt==DocumentFormat.FB2 && isZip ) 603 ext = ".fb2.zip"; 604 else 605 ext = fmt.getExtensions()[0].substring(1); 606 } 607 for (int i=0; i<1000; i++ ) { 608 String fn = fileName + (i==0 ? "" : "(" + i + ")") + "." + ext; 609 File f = new File(outDir, fn); 610 if ( !f.exists() && !f.isDirectory() ) 611 return f; 612 } 613 return null; 614 } downloadBook( final String type, final String url, InputStream is, int contentLength, final String fileName, final boolean isZip )615 private void downloadBook( final String type, final String url, InputStream is, int contentLength, final String fileName, final boolean isZip ) throws Exception { 616 L.d("Download requested: " + type + " " + url + " " + contentLength); 617 DocumentFormat fmt = DocumentFormat.byMimeType(type); 618 if ( fmt==null ) { 619 L.d("Download: unknown type " + type); 620 throw new Exception("Unknown file type " + type); 621 } 622 final File outDir = BackgroundThread.instance().callGUI(new Callable<File>() { 623 @Override 624 public File call() throws Exception { 625 return callback.onDownloadStart(type, url); 626 } 627 }); 628 if ( outDir==null ) { 629 L.d("Cannot find writable location for downloaded file " + url); 630 throw new Exception("Cannot save file " + url); 631 } 632 final File outFile = generateFileName( outDir, fileName, type, isZip ); 633 if ( outFile==null ) { 634 L.d("Cannot generate file name"); 635 throw new Exception("Cannot generate file name"); 636 } 637 L.d("Creating file: " + outFile.getAbsolutePath()); 638 if ( outFile.exists() || !outFile.createNewFile() ) { 639 L.d("Cannot create file " + outFile.getAbsolutePath()); 640 throw new Exception("Cannot create file"); 641 } 642 643 L.d("Download started: " + outFile.getAbsolutePath()); 644 // long lastTs = System.currentTimeMillis(); 645 // int lastPercent = -1; 646 boolean success = false; 647 try (FileOutputStream os = new FileOutputStream(outFile)) { 648 byte[] buf = new byte[16384]; 649 int totalWritten = 0; 650 while (totalWritten<contentLength || contentLength==-1) { 651 int bytesRead = is.read(buf); 652 if ( bytesRead<=0 ) 653 break; 654 os.write(buf, 0, bytesRead); 655 totalWritten += bytesRead; 656 // final int percent = totalWritten * 100 / contentLength; 657 // long ts = System.currentTimeMillis(); 658 // if ( percent!=lastPercent && ts - lastTs > 1500 ) { 659 // L.d("Download progress: " + percent + "%"); 660 // BackgroundThread.instance().postGUI(new Runnable() { 661 // @Override 662 // public void run() { 663 // callback.onDownloadProgress(type, url, percent); 664 // } 665 // }); 666 // } 667 } 668 success = true; 669 } finally { 670 if ( !success ) { 671 if ( outFile.exists() && outFile.isFile() ) { 672 L.w("deleting unsuccessully downloaded file " + outFile); 673 outFile.delete(); 674 } 675 } 676 } 677 L.d("Download finished"); 678 BackgroundThread.instance().executeGUI(() -> callback.onDownloadEnd(type, url, outFile)); 679 } findSubstring( byte[]buf, String str )680 public static int findSubstring( byte[]buf, String str ) { 681 for ( int i=0; i<buf.length-str.length(); i++ ) { 682 boolean found = true; 683 for ( int j=0; j<str.length(); j++ ) 684 if ( str.charAt(j)!=buf[i+j] ) { 685 found = false; 686 break; 687 } 688 if ( found ) 689 return i; 690 } 691 return -1; // not found 692 } 693 encodePassword(String username, String password)694 public static String encodePassword(String username, String password) { 695 return Base64.encodeToString((username + ":" + password).getBytes(), Base64.NO_WRAP); 696 } 697 runInternal()698 public void runInternal() { 699 connection = null; 700 701 boolean itemsLoadedPartially = false; 702 boolean loadNext = false; 703 HashSet<String> visited = new HashSet<String>(); 704 705 do { 706 try { 707 setProgressMessage( url.toString(), -1 ); 708 visited.add(url.toString()); 709 long startTimeStamp = System.currentTimeMillis(); 710 if (!partialDownloadCompleted) { 711 if (delayedProgress != null) 712 delayedProgress.cancel(); 713 delayedProgress = Services.getEngine().showProgressDelayed(0, progressMessage, PROGRESS_DELAY_MILLIS); 714 } 715 URL newURL = url; 716 boolean useOrobotProxy = false; 717 String host = url.getHost(); 718 if (host.endsWith(".onion")) 719 useOrobotProxy = true; 720 String oldAddress = url.toString(); 721 if (oldAddress.startsWith("orobot://")) { 722 newURL = new URL("http://" + oldAddress.substring(9)); // skip orobot:// 723 useOrobotProxy = true; 724 L.d("Converting url - " + oldAddress + " to " + newURL + " for using ORobot proxy"); 725 } else if (oldAddress.startsWith("orobots://")) { 726 newURL = new URL("https://" + oldAddress.substring(10)); // skip orobots:// 727 useOrobotProxy = true; 728 L.d("Converting url - " + oldAddress + " to " + newURL + " for using ORobot proxy"); 729 } 730 Proxy proxy = null; 731 System.setProperty("http.keepAlive", "false"); 732 if (useOrobotProxy) { 733 // Set-up proxy 734 //System.setProperty("http.proxyHost", "127.0.0.1"); 735 //System.setProperty("http.proxyPort", "8118"); 736 //L.d("Using ORobot proxy: " + proxy); 737 proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 8118)); // ORobot proxy running on this device 738 L.d("Using ORobot proxy: " + proxy); 739 } else { 740 //System.clearProperty("http.proxyHost"); 741 //System.clearProperty("http.proxyPort"); 742 } 743 744 URLConnection conn = proxy == null ? newURL.openConnection() : newURL.openConnection(proxy); 745 if ( conn instanceof HttpsURLConnection ) { 746 HttpsURLConnection https = (HttpsURLConnection)conn; 747 748 // Create a trust manager that does not validate certificate chains 749 TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { 750 public java.security.cert.X509Certificate[] getAcceptedIssuers() { 751 return null; 752 } 753 public void checkClientTrusted(X509Certificate[] certs, String authType) { 754 } 755 public void checkServerTrusted(X509Certificate[] certs, String authType) { 756 } 757 } }; 758 // Install the all-trusting trust manager 759 final SSLContext sc = SSLContext.getInstance("SSL"); 760 sc.init(null, trustAllCerts, new java.security.SecureRandom()); 761 HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); 762 763 https.setHostnameVerifier((arg0, arg1) -> true); 764 } 765 if ( !(conn instanceof HttpURLConnection) ) { 766 onError("Only HTTP supported"); 767 return; 768 } 769 connection = (HttpURLConnection)conn; 770 connection.setRequestProperty("User-Agent", "CoolReader/3(Android)"); 771 if ( referer!=null ) 772 connection.setRequestProperty("Referer", referer); 773 connection.setInstanceFollowRedirects(true); 774 connection.setUseCaches(false); 775 776 if (username != null && username.length() > 0 && password != null && password.length() > 0) { 777 connection.setRequestProperty("Authorization", encodePassword(username, password)); 778 Authenticator.setDefault(new Authenticator() { 779 protected PasswordAuthentication getPasswordAuthentication() { 780 return new PasswordAuthentication(username, password.toCharArray()); 781 }}); 782 } 783 784 connection.setAllowUserInteraction(false); 785 connection.setConnectTimeout(CONNECT_TIMEOUT); 786 connection.setReadTimeout(READ_TIMEOUT); 787 connection.setDoInput(true); 788 String fileName = null; 789 String disp = connection.getHeaderField("Content-Disposition"); 790 if ( disp!=null ) { 791 int p = disp.indexOf("filename="); 792 if ( p>0 ) { 793 fileName = disp.substring(p + 9); 794 } 795 } 796 //connection.setDoOutput(true); 797 //connection.set 798 799 int response = -1; 800 801 response = connection.getResponseCode(); 802 if (EXTENDED_LOG) L.d("Response: " + response); 803 if ( response == 301 || response == 302 || response == 307 || response == 303 ) { 804 // redirects 805 String redirect = connection.getHeaderField("Location"); 806 if (null == redirect) { 807 onError("Invalid redirect " + response); 808 return; 809 } 810 L.d("continue with next part: " + url); 811 url = new URL(redirect); 812 if (visited.contains(url.toString())) { 813 onError("Duplicate redirect " + url); 814 return; 815 } 816 loadNext = true; 817 L.d("Response " + response + ": redirect to " + url); 818 continue; 819 } 820 if ( response != 200 ) { 821 onError("Error " + response); 822 return; 823 } 824 825 if (cancelled) 826 break; 827 828 String contentType = connection.getContentType(); 829 String contentEncoding = connection.getContentEncoding(); 830 int contentLen = connection.getContentLength(); 831 //connection.getC 832 if (EXTENDED_LOG) L.d("Entity content length: " + contentLen); 833 if (EXTENDED_LOG) L.d("Entity content type: " + contentType); 834 if (EXTENDED_LOG) L.d("Entity content encoding: " + contentEncoding); 835 setProgressMessage( url.toString(), contentLen ); 836 InputStream is = connection.getInputStream(); 837 if (delayedProgress != null) 838 delayedProgress.cancel(); 839 is = new ProgressInputStream(is, startTimeStamp, progressMessage, contentLen, 80); 840 final int MAX_CONTENT_LEN_TO_BUFFER = 256*1024; 841 boolean isZip = contentType!=null && contentType.equals("application/zip"); 842 if ( expectedType!=null ) 843 contentType = expectedType; 844 else if ( contentLen>0 && contentLen<MAX_CONTENT_LEN_TO_BUFFER) { // autodetect type 845 byte[] buf = new byte[contentLen]; 846 if ( is.read(buf)!=contentLen ) { 847 onError("Wrong content length"); 848 return; 849 } 850 is.close(); 851 is = null; 852 is = new ByteArrayInputStream(buf); 853 if ( findSubstring(buf, "<?xml version=")>=0 && findSubstring(buf, "<feed")>=0 ) 854 contentType = "application/atom+xml"; // override type 855 } 856 if ( contentType.startsWith("application/atom+xml") ) { 857 if (EXTENDED_LOG) L.d("Parsing feed"); 858 parseFeed( is ); 859 itemsLoadedPartially = true; 860 if (handler.docInfo.nextLink!=null && handler.docInfo.nextLink.type.startsWith("application/atom+xml;profile=opds-catalog")) { 861 if (handler.entries.size() < MAX_OPDS_ITEMS) { 862 url = new URL(handler.docInfo.nextLink.href); 863 loadNext = !visited.contains(url.toString()); 864 L.d("continue with next part: " + url); 865 } else { 866 L.d("max item count reached: " + handler.entries.size()); 867 loadNext = false; 868 } 869 } else { 870 loadNext = false; 871 } 872 873 } else { 874 if ( fileName==null ) 875 fileName = defaultFileName; 876 L.d("Downloading book: " + contentEncoding); 877 downloadBook( contentType, url.toString(), is, contentLen, fileName, isZip ); 878 hideProgress(); 879 loadNext = false; 880 itemsLoadedPartially = false; 881 } 882 } catch (Exception e) { 883 L.e("Exception while trying to open URI " + url.toString(), e); 884 if ( progressShown ) 885 Services.getEngine().hideProgress(); 886 onError("Error occured while reading OPDS catalog"); 887 break; 888 } finally { 889 if ( connection!=null ) 890 try { 891 connection.disconnect(); 892 } catch ( Exception e ) { 893 // ignore 894 } 895 } 896 897 partialDownloadCompleted = true; // don't show progress 898 899 if (loadNext && !cancelled) { 900 // partially loaded 901 if ( progressShown ) 902 Services.getEngine().hideProgress(); 903 final ArrayList<EntryInfo> entries = new ArrayList<>(handler.entries); 904 BackgroundThread.instance().executeGUI(() -> { 905 L.d("Parsing is partially. " + handler.entries.size() + " entries found -- updating view"); 906 if (!callback.onEntries(handler.docInfo, entries)) 907 cancel(); 908 }); 909 } 910 } while (loadNext && !cancelled); 911 if (delayedProgress != null) 912 delayedProgress.cancel(); 913 hideProgress(); 914 if (itemsLoadedPartially && !cancelled) { 915 BackgroundThread.instance().executeGUI(() -> { 916 L.d("Parsing is finished successfully. " + handler.entries.size() + " entries found"); 917 hideProgress(); 918 if (!callback.onFinish(handler.docInfo, handler.entries)) 919 cancel(); 920 }); 921 } 922 } 923 run()924 public void run() { 925 BackgroundThread.instance().postBackground(() -> { 926 try { 927 runInternal(); 928 } catch ( Exception e ) { 929 L.e("exception while opening OPDS", e); 930 } 931 }); 932 } 933 cancel()934 public void cancel() { 935 if (!cancelled) { 936 L.d("cancelling current download task"); 937 cancelled = true; 938 } 939 } 940 941 volatile private boolean cancelled = false; 942 943 private boolean progressShown = false; 944 945 private boolean partialDownloadCompleted = false; 946 947 public class ProgressInputStream extends InputStream { 948 949 private static final int TIMEOUT = 1500; 950 951 private final InputStream sourceStream; 952 private final int totalSize; 953 private final String progressMessage; 954 private long lastUpdate; 955 private int lastPercent; 956 private int maxPercentToStartShowingProgress; 957 private int bytesRead; 958 ProgressInputStream( InputStream sourceStream, long startTimeStamp, String progressMessage, int totalSize, int maxPercentToStartShowingProgress )959 public ProgressInputStream( InputStream sourceStream, long startTimeStamp, String progressMessage, int totalSize, int maxPercentToStartShowingProgress ) { 960 this.sourceStream = sourceStream; 961 this.totalSize = totalSize; 962 this.maxPercentToStartShowingProgress = maxPercentToStartShowingProgress * 100; 963 this.progressMessage = progressMessage; 964 this.lastUpdate = startTimeStamp; 965 this.bytesRead = 0; 966 } 967 updateProgress()968 private void updateProgress() { 969 long ts = System.currentTimeMillis(); 970 long delay = ts - lastUpdate; 971 if ( delay > TIMEOUT ) { 972 lastUpdate = ts; 973 int percent = 0; 974 if ( totalSize>0 ) { 975 percent = bytesRead * 100 / totalSize * 100; 976 } 977 if ( !partialDownloadCompleted && (!progressShown || percent!=lastPercent) && (progressShown || percent<maxPercentToStartShowingProgress || delay > TIMEOUT*2 ) ) { 978 Services.getEngine().showProgress(percent, progressMessage); 979 lastPercent = percent; 980 progressShown = true; 981 } 982 } 983 984 } 985 986 @Override read()987 public int read() throws IOException { 988 bytesRead++; 989 updateProgress(); 990 return sourceStream.read(); 991 } 992 993 @Override close()994 public void close() throws IOException { 995 super.close(); 996 } 997 } 998 999 } 1000 private static DownloadTask currentTask; create(CoolReader coolReader, URL uri, String defaultFileName, String expectedType, String referer, DownloadCallback callback, String username, String password)1001 public static DownloadTask create(CoolReader coolReader, URL uri, String defaultFileName, String expectedType, String referer, DownloadCallback callback, String username, String password) { 1002 if (currentTask != null) 1003 currentTask.cancel(); 1004 final DownloadTask task = new DownloadTask(coolReader, uri, defaultFileName, expectedType, referer, callback, username, password); 1005 currentTask = task; 1006 return task; 1007 } 1008 1009 static class SubstTable { 1010 private final int startChar; 1011 private final String[] replacements; SubstTable( int startChar, String[] replacements )1012 public SubstTable( int startChar, String[] replacements ) { 1013 this.startChar = startChar; 1014 this.replacements = replacements; 1015 } isInRange( char ch )1016 boolean isInRange( char ch ) { 1017 return ch>=startChar && ch<startChar + replacements.length; 1018 } get( char ch )1019 String get( char ch ) { 1020 return (ch>=startChar && ch<startChar + replacements.length) ? replacements[ch - startChar] : ""; 1021 } 1022 } 1023 1024 public static final int PROGRESS_DELAY_MILLIS = 2000; 1025 public static final int MAX_OPDS_ITEMS = 1000; 1026 } 1027