/* * Copyright 2012 GitHub Inc. * * 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.github.mobile.persistence; import android.accounts.Account; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; import android.util.Log; import com.github.mobile.RequestFuture; import com.github.mobile.RequestReader; import com.github.mobile.RequestWriter; import com.github.mobile.accounts.AuthenticatedUserTask; import com.github.mobile.core.issue.IssueFilter; import com.github.mobile.persistence.OrganizationRepositories.Factory; import com.google.inject.Inject; import com.google.inject.name.Named; import java.io.File; import java.io.IOException; import java.text.MessageFormat; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import org.eclipse.egit.github.core.Repository; import org.eclipse.egit.github.core.User; /** * Manager cache for an account */ public class AccountDataManager { private static final String TAG = "AccountDataManager"; private static final Executor EXECUTOR = Executors.newFixedThreadPool(10); /** * Format version to bump if serialization format changes and cache should * be ignored */ private static final int FORMAT_VERSION = 4; @Inject private Context context; @Inject private DatabaseCache dbCache; @Inject private Factory allRepos; @Inject private Organizations userAndOrgsResource; @Inject @Named("cacheDir") private File root; /** * @return context */ public Context getContext() { return context; } /** * Read data from file * * @param file * @return data */ @SuppressWarnings("unchecked") private <V> V read(final File file) { long start = System.currentTimeMillis(); long length = file.length(); Object data = new RequestReader(file, FORMAT_VERSION).read(); if (data != null) Log.d(TAG, MessageFormat.format( "Cache hit to {0}, {1} ms to load {2} bytes", file.getName(), (System.currentTimeMillis() - start), length)); return (V) data; } /** * Write data to file * * @param file * @param data * @return this manager */ private AccountDataManager write(File file, Object data) { new RequestWriter(file, FORMAT_VERSION).write(data); return this; } /** * Query tables for columns * * @param helper * @param tables * @param columns * @return cursor */ protected Cursor query(SQLiteOpenHelper helper, String tables, String[] columns) { return query(helper, tables, columns, null, null); } /** * Query tables for columns * * @param helper * @param tables * @param columns * @param selection * @param selectionArgs * @return cursor */ protected Cursor query(SQLiteOpenHelper helper, String tables, String[] columns, String selection, String[] selectionArgs) { SQLiteQueryBuilder builder = new SQLiteQueryBuilder(); builder.setTables(tables); return builder.query(helper.getReadableDatabase(), columns, selection, selectionArgs, null, null, null); } /** * Get organizations * <p/> * This method may perform file and/or network I/O and should never be * called on the UI-thread * * @param forceReload * @return list of user and Orgs * @throws IOException */ public List<User> getOrgs(boolean forceReload) throws IOException { return forceReload ? dbCache.requestAndStore(userAndOrgsResource) : dbCache.loadOrRequest(userAndOrgsResource); } /** * Get repositories for given {@link User} * <p/> * This method may perform network I/O and should never be called on the * UI-thread * * @param user * @param forceReload * if true, cached data will not be returned * @return list of repositories * @throws IOException */ public List<Repository> getRepos(final User user, boolean forceReload) throws IOException { OrganizationRepositories resource = allRepos.under(user); return forceReload ? dbCache.requestAndStore(resource) : dbCache .loadOrRequest(resource); } /** * Get bookmarked issue filters * <p/> * This method may perform network I/O and should never be called on the * UI-thread * * @return non-null but possibly empty collection of issue filters */ public Collection<IssueFilter> getIssueFilters() { final File cache = new File(root, "issue_filters.ser"); Collection<IssueFilter> cached = read(cache); if (cached != null) return cached; return Collections.emptyList(); } /** * Get bookmarked issue filters * * @param requestFuture */ public void getIssueFilters( final RequestFuture<Collection<IssueFilter>> requestFuture) { new AuthenticatedUserTask<Collection<IssueFilter>>(context, EXECUTOR) { @Override public Collection<IssueFilter> run(Account account) throws Exception { return getIssueFilters(); } @Override protected void onSuccess(Collection<IssueFilter> filters) throws Exception { requestFuture.success(filters); } }.execute(); } /** * Add issue filter to store * <p/> * This method may perform file I/O and should never be called on the * UI-thread * * @param filter */ public void addIssueFilter(IssueFilter filter) { final File cache = new File(root, "issue_filters.ser"); Collection<IssueFilter> filters = read(cache); if (filters == null) filters = new HashSet<IssueFilter>(); if (filters.add(filter)) write(cache, filters); } /** * Add issue filter to store * * @param filter * @param requestFuture */ public void addIssueFilter(final IssueFilter filter, final RequestFuture<IssueFilter> requestFuture) { new AuthenticatedUserTask<IssueFilter>(context, EXECUTOR) { @Override public IssueFilter run(Account account) throws Exception { addIssueFilter(filter); return filter; } @Override protected void onSuccess(IssueFilter filter) throws Exception { requestFuture.success(filter); } @Override protected void onException(Exception e) throws RuntimeException { Log.d(TAG, "Exception adding issue filter", e); } }.execute(); } /** * Add issue filter from store * <p/> * This method may perform file I/O and should never be called on the * UI-thread * * @param filter */ public void removeIssueFilter(IssueFilter filter) { final File cache = new File(root, "issue_filters.ser"); Collection<IssueFilter> filters = read(cache); if (filters != null && filters.remove(filter)) write(cache, filters); } /** * Remove issue filter from store * * @param filter * @param requestFuture */ public void removeIssueFilter(final IssueFilter filter, final RequestFuture<IssueFilter> requestFuture) { new AuthenticatedUserTask<IssueFilter>(context, EXECUTOR) { @Override public IssueFilter run(Account account) throws Exception { removeIssueFilter(filter); return filter; } @Override protected void onSuccess(IssueFilter filter) throws Exception { requestFuture.success(filter); } @Override protected void onException(Exception e) throws RuntimeException { Log.d(TAG, "Exception removing issue filter", e); } }.execute(); } }