blob: 6a158dfa5ef149af1fe1071793ec04cc7a1eac78 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* 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.
*/
/*
* Copyright (C) 2017 The Android Open Source Project
*
* 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.android.sample.githubbrowser.viewmodel;
import android.os.AsyncTask;
import android.support.annotation.MainThread;
import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import com.android.sample.githubbrowser.data.GeneralRepoSearchData;
import com.android.sample.githubbrowser.data.RepositoryData;
import com.android.sample.githubbrowser.data.SearchQueryData;
import com.android.sample.githubbrowser.db.GithubDao;
import com.android.sample.githubbrowser.db.GithubDatabase;
import com.android.sample.githubbrowser.di.AppComponent;
import com.android.sample.githubbrowser.model.AuthTokenModel;
import com.android.sample.githubbrowser.network.GithubNetworkManager;
import com.android.sample.githubbrowser.util.ChainedLiveData;
import com.android.sample.githubbrowser.viewmodel.InjectableViewModel;
import com.android.support.lifecycle.LiveData;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import javax.inject.Inject;
/**
* View model for repository list data.
*/
public class RepositoryListModel extends InjectableViewModel {
/** Constant for the initial loading state. */
public static final int STATE_INITIAL_LOADING = 0;
/** Constant for the empty / no data state. */
public static final int STATE_EMPTY = 1;
/** Constant for the data state. */
public static final int STATE_DATA = 2;
/** Constant for the error state. */
public static final int STATE_ERROR = 3;
private String mSearchTerm;
private final ChainedLiveData<List<RepositoryData>> mRepositoryListLiveData
= new ChainedLiveData<>();
private final LiveData<Integer> mStateLiveData = new LiveData<>();
private AtomicBoolean mHasNetworkRequestPending = new AtomicBoolean(false);
private GithubNetworkManager.Cancelable mCurrentNetworkCall;
private SearchQueryData mSearchQueryData;
private AtomicInteger mLastRequestedIndex = new AtomicInteger(0);
@Inject
GithubNetworkManager mGithubNetworkManager;
@Inject
GithubDatabase mDatabase;
@Inject
AuthTokenModel mAuthTokenModel;
@Override
void inject(AppComponent appComponent) {
appComponent.inject(this);
}
/**
* Returns true if the current search term is not empty.
*/
public boolean hasSearchTerm() {
return !TextUtils.isEmpty(mSearchTerm);
}
/**
* Sets new search term.
*/
@MainThread
public void setSearchTerm(String searchTerm) {
mSearchTerm = searchTerm;
if (mCurrentNetworkCall != null) {
mCurrentNetworkCall.cancel();
}
final GithubDao githubDao = mDatabase.getGithubDao();
// Get the LiveData wrapper around the list of repositories that match our current
// search query. The wrapped list will be updated on every successful network request
// that is performed for data that is not available in our database.
mRepositoryListLiveData.setBackingLiveData(githubDao.getRepositories(mSearchTerm));
mStateLiveData.setValue(STATE_INITIAL_LOADING);
mHasNetworkRequestPending.set(false);
new AsyncTask<String, Void, Void>() {
@Override
protected Void doInBackground(String... params) {
// Get data about locally persisted results of our current search query. Note that
// since this is working with a disk-based database, we're running off the main
// thread.
mSearchQueryData = githubDao.getSearchQueryData(
params[0], SearchQueryData.GENERAL_REPOSITORIES);
if (mSearchQueryData == null) {
// This query has not been performed before - initialize an entry in the
// database. TODO - consult the timestamp of network requests for staleness.
mSearchQueryData = new SearchQueryData();
mSearchQueryData.searchQuery = params[0];
mSearchQueryData.searchKind = SearchQueryData.GENERAL_REPOSITORIES;
mSearchQueryData.numberOfFetchedItems = -1;
githubDao.update(mSearchQueryData);
}
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
fetchNextPage();
}
}.execute(mSearchTerm);
}
private void fetchNextPage() {
if (mSearchQueryData == null) {
// Not ready to fetch yet.
return;
}
// Do we have data in the database?
if (mSearchQueryData.numberOfFetchedItems >= mLastRequestedIndex.get()) {
// We already have the data stored (and retrieved) from database.
mStateLiveData.setValue(STATE_DATA);
return;
}
if (mHasNetworkRequestPending.get()) {
// Previous request still processing
return;
}
if (mSearchQueryData.hasNoMoreData) {
// We don't have any more results
if (mSearchQueryData.numberOfFetchedItems <= 0) {
mStateLiveData.setValue(STATE_EMPTY);
}
return;
}
mHasNetworkRequestPending.set(true);
mCurrentNetworkCall = mGithubNetworkManager.listRepositories(
mSearchTerm, mSearchQueryData.indexOfLastFetchedPage + 1,
new GithubNetworkManager.NetworkCallListener<List<RepositoryData>>() {
@Override
public void onLoadEmpty(int httpCode) {
switch (httpCode) {
case 404:
// No such user
mStateLiveData.setValue(STATE_EMPTY);
break;
default:
mStateLiveData.setValue(STATE_ERROR);
}
}
@Override
public void onLoadSuccess(List<RepositoryData> data) {
new AsyncTask<RepositoryData, Void, Void>() {
@Override
protected Void doInBackground(RepositoryData... params) {
// Note that since we're going to be inserting data into disk-based
// database, we need to be running off the main thread.
processNewPageOfData(params);
return null;
}
}.execute(data.toArray(new RepositoryData[data.size()]));
}
@Override
public void onLoadFailure() {
mStateLiveData.setValue(STATE_ERROR);
}
});
}
@WorkerThread
private void processNewPageOfData(RepositoryData... data) {
try {
mDatabase.beginTransaction();
int newDataCount = data.length;
final GithubDao githubDao = mDatabase.getGithubDao();
final int indexOfFirstData = mSearchQueryData.numberOfFetchedItems;
// Update the metadata about our current search query (in the database)
if (newDataCount == 0) {
mSearchQueryData.hasNoMoreData = true;
} else {
if (mSearchQueryData.indexOfLastFetchedPage == 0) {
mSearchQueryData.timestamp = System.currentTimeMillis();
}
mSearchQueryData.indexOfLastFetchedPage++;
mSearchQueryData.numberOfFetchedItems += newDataCount;
}
githubDao.update(mSearchQueryData);
if (newDataCount > 0) {
// Insert entries for the newly loaded repositories in two places:
// 1. The table that stores repository IDs that match a specific query.
// 2. The table that stores full data on each individual repository.
// This way we don't store multiple full entries for the same repository
// that happens to match two or more search queries.
GeneralRepoSearchData[] generalRepoSearchDataArray =
new GeneralRepoSearchData[newDataCount];
for (int i = 0; i < newDataCount; i++) {
generalRepoSearchDataArray[i] = new GeneralRepoSearchData();
generalRepoSearchDataArray[i].searchQuery = mSearchTerm;
generalRepoSearchDataArray[i].resultIndex = indexOfFirstData + i;
generalRepoSearchDataArray[i].repoId = data[i].id;
}
githubDao.insert(generalRepoSearchDataArray);
githubDao.insert(data);
}
mDatabase.setTransactionSuccessful();
} finally {
mDatabase.endTransaction();
}
mHasNetworkRequestPending.set(false);
mStateLiveData.postValue(
(mSearchQueryData.numberOfFetchedItems <= 0) && mSearchQueryData.hasNoMoreData
? STATE_EMPTY : STATE_DATA);
}
/**
* Fetches data at specified index if data does not exist yet.
*/
public void fetchAtIndexIfNecessary(int index) {
if (mSearchQueryData == null) {
// If we're here, we've been asked to start fetching items before we've retrieved
// the top-level metadata for our search. Save the requested index and return. Once
// that metadata is fetched off the main thread in the AsyncTask executed in
// setSearchTerms, we'll call fetchNextPage().
mLastRequestedIndex.set(index);
return;
}
if (mHasNetworkRequestPending.get() || mSearchQueryData.hasNoMoreData) {
// Previous request still processing or no more results
return;
}
mLastRequestedIndex.set(index);
fetchNextPage();
}
/**
* Resumes loading of data in this model.
*/
public void resumeLoading() {
fetchNextPage();
}
/**
* Returns the {@LiveData} object that wraps the current list of repos that matches the last
* set search term.
*/
public LiveData<List<RepositoryData>> getRepositoryListLiveData() {
return mRepositoryListLiveData;
}
/**
* Returns the {@LiveData} object that wraps the current data state.
*/
public LiveData<Integer> getStateLiveData() {
return mStateLiveData;
}
}