/* * Copyright 2014 sonaive.com. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.sonaive.v2ex.io; import android.content.ContentProviderOperation; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.provider.BaseColumns; import android.text.TextUtils; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.sonaive.v2ex.Config; import com.sonaive.v2ex.io.model.Feed; import com.sonaive.v2ex.provider.V2exContract; import com.sonaive.v2ex.sync.api.Api; import com.sonaive.v2ex.util.ModelUtils; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import static com.sonaive.v2ex.util.LogUtils.LOGD; import static com.sonaive.v2ex.util.LogUtils.LOGW; import static com.sonaive.v2ex.util.LogUtils.LOGE; import static com.sonaive.v2ex.util.LogUtils.makeLogTag; /** * Created by liutao on 12/13/14. */ public class FeedsHandler extends JSONHandler { private static final String TAG = makeLogTag(FeedsHandler.class); private HashMap<String, Feed> mFeeds = new HashMap<>(); public FeedsHandler(Context context) { super(context); } @Override public void makeContentProviderOperations(ArrayList<ContentProviderOperation> list) { Uri uri = V2exContract.Feeds.CONTENT_URI; HashMap<String, String> feedHashcodes = loadFeedsHashcodes(); HashSet<String> feedsToKeep = new HashSet<>(); boolean isIncrementalUpdate = feedHashcodes != null && feedHashcodes.size() > 0; if (isIncrementalUpdate) { LOGD(TAG, "Doing incremental update for feeds."); } else { LOGD(TAG, "Doing FULL (non incremental) update for feeds."); list.add(ContentProviderOperation.newDelete(uri).build()); } int updatedFeeds = 0; for (Feed feed : mFeeds.values()) { String hashCode = feed.getImportHashcode(); feedsToKeep.add(String.valueOf(feed.id)); // add feed, if necessary if (!isIncrementalUpdate || !feedHashcodes.containsKey(String.valueOf(feed.id)) || !feedHashcodes.get(String.valueOf(feed.id)).equals(hashCode)) { ++updatedFeeds; boolean isNew = !isIncrementalUpdate || !feedHashcodes.containsKey(String.valueOf(feed.id)); buildFeed(isNew, feed, list); } } // Since the api only provide one page of newest feeds. We just insert // or update feed to local database. Once the rows of feeds table exceed the threshold // let's say 2000 rows, it's time to clean up. Delete all older rows. int deletedFeeds = 0; if (feedHashcodes != null && feedHashcodes.size() >= Config.THRESHOLD) { for (String feedId : feedHashcodes.keySet()) { if (!feedsToKeep.contains(feedId)) { buildDeleteOperation(feedId, list); ++deletedFeeds; } } } LOGD(TAG, "Feeds: " + (isIncrementalUpdate ? "INCREMENTAL" : "FULL") + " update. " + updatedFeeds + " to update, " + deletedFeeds + " to delete, New total: " + mFeeds.size()); } @Override public void process(JsonElement element) { for (Feed feed : new Gson().fromJson(element, Feed[].class)) { mFeeds.put(String.valueOf(feed.id), feed); } } @Override public String getBody(Bundle data) { if (data != null) { return data.getString(Api.ARG_RESULT); } return ""; } private void buildFeed(boolean isInsert, Feed feed, ArrayList<ContentProviderOperation> list) { Uri allVideosUri = V2exContract.addCallerIsSyncAdapterParameter( V2exContract.Feeds.CONTENT_URI); Uri thisVideoUri = V2exContract.addCallerIsSyncAdapterParameter( V2exContract.Feeds.buildFeedUri(String.valueOf(feed.id))); ContentProviderOperation.Builder builder; if (isInsert) { builder = ContentProviderOperation.newInsert(allVideosUri); } else { builder = ContentProviderOperation.newUpdate(thisVideoUri); } if (TextUtils.isEmpty(String.valueOf(feed.id))) { LOGW(TAG, "Ignoring feed with missing feed ID."); return; } list.add(builder.withValue(V2exContract.Feeds.FEED_ID, feed.id) .withValue(V2exContract.Feeds.FEED_TITLE, feed.title) .withValue(V2exContract.Feeds.FEED_URL, feed.url) .withValue(V2exContract.Feeds.FEED_CONTENT, feed.content) .withValue(V2exContract.Feeds.FEED_CONTENT_RENDERED, feed.content_rendered) .withValue(V2exContract.Feeds.FEED_REPLIES, feed.replies) .withValue(V2exContract.Feeds.FEED_MEMBER, ModelUtils.serializeMember(feed.member)) .withValue(V2exContract.Feeds.FEED_NODE, ModelUtils.serializeNode(feed.node)) .withValue(V2exContract.Feeds.FEED_CREATED, feed.created) .withValue(V2exContract.Feeds.FEED_LAST_MODIFIED, feed.last_modified) .withValue(V2exContract.Feeds.FEED_LAST_TOUCHED, feed.last_touched) .withValue(V2exContract.Feeds.FEED_IMPORT_HASHCODE, feed.getImportHashcode()) .build()); } private void buildDeleteOperation(String feedId, ArrayList<ContentProviderOperation> list) { Uri uri = V2exContract.addCallerIsSyncAdapterParameter( V2exContract.Feeds.buildFeedUri(feedId)); list.add(ContentProviderOperation.newDelete(uri).build()); } private HashMap<String, String> loadFeedsHashcodes() { Uri uri = V2exContract.Feeds.CONTENT_URI; Cursor cursor = mContext.getContentResolver().query(uri, FeedHashcodeQuery.PROJECTION, null, null, null); if (cursor == null) { LOGE(TAG, "Error querying feed hashcodes (got null cursor)"); return null; } if (cursor.getCount() < 1) { LOGE(TAG, "Error querying feed hashcodes (no records returned)"); cursor.close(); return null; } HashMap<String, String> result = new HashMap<>(); while (cursor.moveToNext()) { String feedId = cursor.getString(FeedHashcodeQuery.FEED_ID); String hashcode = cursor.getString(FeedHashcodeQuery.FEED_IMPORT_HASHCODE); result.put(feedId, hashcode == null ? "" : hashcode); } cursor.close(); return result; } private interface FeedHashcodeQuery { String[] PROJECTION = { BaseColumns._ID, V2exContract.Feeds.FEED_ID, V2exContract.Feeds.FEED_IMPORT_HASHCODE }; final int _ID = 0; final int FEED_ID = 1; final int FEED_IMPORT_HASHCODE = 2; } }