package org.wikipedia.savedpages;
import android.app.IntentService;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.wikipedia.WikipediaApp;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.dataclient.okhttp.OkHttpConnectionFactory;
import org.wikipedia.dataclient.okhttp.cache.DiskLruCacheUtil;
import org.wikipedia.dataclient.okhttp.cache.SaveHeader;
import org.wikipedia.dataclient.page.PageClient;
import org.wikipedia.dataclient.page.PageClientFactory;
import org.wikipedia.dataclient.page.PageLead;
import org.wikipedia.dataclient.page.PageRemaining;
import org.wikipedia.html.ImageTagParser;
import org.wikipedia.html.PixelDensityDescriptorParser;
import org.wikipedia.page.PageTitle;
import org.wikipedia.readinglist.page.ReadingListPageRow;
import org.wikipedia.readinglist.page.database.ReadingListPageDao;
import org.wikipedia.readinglist.page.database.disk.ReadingListPageDiskRow;
import org.wikipedia.util.DimenUtil;
import org.wikipedia.util.FileUtil;
import org.wikipedia.util.UriUtil;
import org.wikipedia.util.log.L;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import okhttp3.CacheControl;
import okhttp3.CacheDelegate;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.internal.cache.DiskLruCache;
import retrofit2.Call;
import static org.wikipedia.dataclient.okhttp.OkHttpConnectionFactory.SAVE_CACHE;
public class SavedPageSyncService extends IntentService {
@NonNull private ReadingListPageDao dao;
@NonNull private final CacheDelegate cacheDelegate = new CacheDelegate(SAVE_CACHE);
@NonNull private final PageImageUrlParser pageImageUrlParser
= new PageImageUrlParser(new ImageTagParser(), new PixelDensityDescriptorParser());
private long blockSize;
public SavedPageSyncService() {
super("SavedPageSyncService");
dao = ReadingListPageDao.instance();
blockSize = FileUtil.blockSize(cacheDelegate.diskLruCache().getDirectory());
}
@Override protected void onHandleIntent(@Nullable Intent intent) {
List<ReadingListPageDiskRow> queue = new ArrayList<>();
Collection<ReadingListPageDiskRow> rows = dao.startDiskTransaction();
for (ReadingListPageDiskRow row : rows) {
switch (row.status()) {
case UNSAVED:
case DELETED:
deleteRow(row);
break;
case OUTDATED:
queue.add(row);
break;
case ONLINE:
case SAVED:
// SavedPageSyncService observes all list changes. No transaction is pending
// when the row is online or saved.
break;
default:
throw new UnsupportedOperationException("Invalid disk row status: "
+ row.status().name());
}
}
saveNewEntries(queue);
}
private void deleteRow(@NonNull ReadingListPageDiskRow row) {
ReadingListPageRow dat = row.dat();
PageTitle pageTitle = makeTitleFrom(row);
if (dat != null && pageTitle != null) {
PageLead lead = null;
Call<PageLead> leadCall = reqPageLead(CacheControl.FORCE_CACHE, pageTitle);
try {
lead = leadCall.execute().body();
} catch (IOException ignore) { }
if (lead != null) {
for (String url : pageImageUrlParser.parse(lead)) {
cacheDelegate.remove(saveImageReq(pageTitle.getWikiSite(), url));
}
cacheDelegate.remove(leadCall.request());
}
Call<PageRemaining> sectionsCall = reqPageSections(CacheControl.FORCE_CACHE, pageTitle);
PageRemaining sections = null;
try {
sections = sectionsCall.execute().body();
} catch (IOException ignore) { }
if (sections != null) {
for (String url : pageImageUrlParser.parse(sections)) {
cacheDelegate.remove(saveImageReq(pageTitle.getWikiSite(), url));
}
cacheDelegate.remove(sectionsCall.request());
}
}
dao.completeDiskTransaction(row);
}
private void saveNewEntries(List<ReadingListPageDiskRow> queue) {
while (!queue.isEmpty()) {
ReadingListPageDiskRow row = queue.get(0);
PageTitle pageTitle = makeTitleFrom(row);
if (pageTitle == null) {
// todo: won't this fail forever or until the page is marked unsaved / removed somehow?
dao.failDiskTransaction(queue);
break;
}
AggregatedResponseSize size;
try {
size = savePageFor(pageTitle);
} catch (IOException e) {
dao.failDiskTransaction(queue);
break;
}
ReadingListPageDiskRow rowWithUpdatedSize = new ReadingListPageDiskRow(row,
ReadingListPageRow.builder().copy(row.dat()).logicalSize(size.logicalSize()).physicalSize(size.physicalSize()).build());
dao.completeDiskTransaction(rowWithUpdatedSize);
queue.remove(row);
}
}
@NonNull private AggregatedResponseSize savePageFor(@NonNull PageTitle pageTitle) throws IOException {
AggregatedResponseSize size = new AggregatedResponseSize(0, 0, 0);
Call<PageLead> leadCall = reqPageLead(null, pageTitle);
Call<PageRemaining> sectionsCall = reqPageSections(null, pageTitle);
retrofit2.Response<PageLead> leadRsp = leadCall.execute();
size = size.add(responseSize(leadRsp));
retrofit2.Response<PageRemaining> sectionsRsp = sectionsCall.execute();
size = size.add(responseSize(sectionsRsp));
Set<String> imageUrls = new HashSet<>(pageImageUrlParser.parse(leadRsp.body()));
imageUrls.addAll(pageImageUrlParser.parse(sectionsRsp.body()));
size = size.add(reqSaveImage(pageTitle.getWikiSite(), imageUrls));
String title = pageTitle.getPrefixedText();
L.i("Saved page " + title + " (" + size + ")");
return size;
}
@NonNull private Call<PageLead> reqPageLead(@Nullable CacheControl cacheControl,
@NonNull PageTitle pageTitle) {
PageClient client = newPageClient(pageTitle);
String title = pageTitle.getPrefixedText();
int thumbnailWidth = DimenUtil.calculateLeadImageWidth();
boolean noImages = !WikipediaApp.getInstance().isImageDownloadEnabled();
PageClient.CacheOption cacheOption = PageClient.CacheOption.SAVE;
return client.lead(cacheControl, cacheOption, title, thumbnailWidth, noImages);
}
@NonNull private Call<PageRemaining> reqPageSections(@Nullable CacheControl cacheControl,
@NonNull PageTitle pageTitle) {
PageClient client = newPageClient(pageTitle);
String title = pageTitle.getPrefixedText();
boolean noImages = !WikipediaApp.getInstance().isImageDownloadEnabled();
PageClient.CacheOption cacheOption = PageClient.CacheOption.SAVE;
return client.sections(cacheControl, cacheOption, title, noImages);
}
private AggregatedResponseSize reqSaveImage(@NonNull WikiSite wiki, @NonNull Iterable<String> urls) throws IOException {
AggregatedResponseSize size = new AggregatedResponseSize(0, 0, 0);
for (String url : urls) {
size = size.add(reqSaveImage(wiki, url));
}
return size;
}
@NonNull private ResponseSize reqSaveImage(@NonNull WikiSite wiki, @NonNull String url) throws IOException {
Request request = saveImageReq(wiki, url);
Response rsp = OkHttpConnectionFactory.getClient().newCall(request).execute();
// Note: raw non-Retrofit usage of OkHttp Requests requires that the Response body is read
// for the cache to be written.
rsp.body().close();
// Size must be checked after the body has been written.
return responseSize(rsp);
}
@NonNull private Request saveImageReq(@NonNull WikiSite wiki, @NonNull String url) {
return new Request
.Builder()
.addHeader(SaveHeader.FIELD, SaveHeader.VAL_ENABLED)
.url(UriUtil.resolveProtocolRelativeUrl(wiki, url))
.build();
}
@NonNull private ResponseSize responseSize(@NonNull Response rsp) {
return responseSize(rsp.request());
}
@NonNull private ResponseSize responseSize(@NonNull retrofit2.Response rsp) {
return responseSize(rsp.raw().request());
}
@NonNull private ResponseSize responseSize(@NonNull Request req) {
return responseSize(cacheDelegate.entry(req));
}
@NonNull private ResponseSize responseSize(@Nullable DiskLruCache.Snapshot snapshot) {
long metadataSize = DiskLruCacheUtil.okHttpResponseMetadataSize(snapshot);
long bodySize = DiskLruCacheUtil.okHttpResponseBodySize(snapshot);
return new ResponseSize(metadataSize, bodySize);
}
@Nullable private PageTitle makeTitleFrom(@NonNull ReadingListPageDiskRow row) {
ReadingListPageRow pageRow = row.dat();
if (pageRow == null) {
return null;
}
String namespace = pageRow.namespace().toLegacyString();
return new PageTitle(namespace, pageRow.title(), pageRow.wikiSite());
}
@NonNull private PageClient newPageClient(@NonNull PageTitle title) {
return PageClientFactory.create(title.getWikiSite(), title.namespace());
}
private static class AggregatedResponseSize {
private final long physicalSize;
private final long logicalSize;
private final int responsesAggregated;
AggregatedResponseSize(long physicalSize, long logicalSize, int responsesAggregated) {
this.physicalSize = physicalSize;
this.logicalSize = logicalSize;
this.responsesAggregated = responsesAggregated;
}
@Override public String toString() {
return "responses=" + responsesAggregated() + " physical=" + physicalSize() + "B logical=" + logicalSize() + "B";
}
long physicalSize() {
return physicalSize;
}
// The size on disk.
long logicalSize() {
return logicalSize;
}
int responsesAggregated() {
return responsesAggregated;
}
@NonNull AggregatedResponseSize add(@NonNull ResponseSize size) {
return new AggregatedResponseSize(physicalSize + size.physicalSize(),
logicalSize + size.logicalSize(), responsesAggregated() + 1);
}
@NonNull AggregatedResponseSize add(@NonNull AggregatedResponseSize size) {
return new AggregatedResponseSize(physicalSize + size.physicalSize(),
logicalSize + size.logicalSize(), responsesAggregated() + size.responsesAggregated());
}
}
private class ResponseSize {
private final long metadataSize;
private final long bodySize;
ResponseSize(long metadataSize, long bodySize) {
this.metadataSize = metadataSize;
this.bodySize = bodySize;
}
@Override public String toString() {
return "physical metadata=" + metadataSize + "B physical body=" + bodySize
+ "B physical=" + physicalSize() + "B logical=" + logicalSize() + "B";
}
long physicalSize() {
return metadataSize + bodySize;
}
long logicalSize() {
return FileUtil.physicalToLogicalSize(metadataSize, blockSize)
+ FileUtil.physicalToLogicalSize(bodySize, blockSize);
}
}
}