私はAndroid開発にかなり慣れていません。私はドキュメンテーションを研究し、これとあれを行う方法についてのチュートリアルをたくさん読みましたが、1 つの問題がまだ私を悩ませています。
私のプロジェクトには、著者、小説、章の 3 つのエンティティがあります。
特定のサーバーで公開された最新のチャプターのリストを含むフラグメントがあり、それが一番下までスクロールされると (LoaderManager を使用して)、より多くのアイテムを動的にロードします。メカニズムは次のようなものです: フラグメントは限られた数のレコードについて SQLite データベースにクエリを実行し、URI を担当するコンテンツ プロバイダーはサービス (私自身) を呼び出して、より多くのチャプターをダウンロードしてデータベースに保存します。その特定の uri のコンテンツ オブザーバーに通知が送信され、フラグメントのリストが更新されます。
リストを下にスクロールするたびに (DB から新しいデータをロードしようとしているときに、アプリが新しいコンテンツをダウンロードして挿入していると思います)、次のエラーが発生します。
ERROR/AndroidRuntime(689): FATAL EXCEPTION: IntentService[DatabaseService]
java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase: /data/data/cz.muni.fi.WebNovelReader/databases/Live
at android.database.sqlite.SQLiteClosable.acquireReference(SQLiteClosable.java:55)
at android.database.sqlite.SQLiteDatabase.beginTransaction(SQLiteDatabase.java:503)
at android.database.sqlite.SQLiteDatabase.beginTransaction(SQLiteDatabase.java:416)
at cz.muni.fi.WebNovelReader.Downloader.LiveProvider.bulkInsertOrUpdate(LiveProvider.java:2040)
at cz.muni.fi.WebNovelReader.Downloader.LiveProvider.bulkInsert(LiveProvider.java:1804)
at android.content.ContentProvider$Transport.bulkInsert(ContentProvider.java:207)
at android.content.ContentResolver.bulkInsert(ContentResolver.java:925)
at cz.muni.fi.WebNovelReader.Downloader.DatabaseService.onHandleIntent(DatabaseService.java:90)
at android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:65)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.os.HandlerThread.run(HandlerThread.java:60)
行 2040 は、以下のソース コードでマークされています。さらにデータをロード (ダウンロードおよび挿入) しているときに上にスクロールしようとすると、別のエラーが発生します。別のエラーが発生します。
ERROR/AndroidRuntime(619): FATAL EXCEPTION: ModernAsyncTask #4
java.lang.RuntimeException: An error occured while executing doInBackground()
at android.support.v4.content.ModernAsyncTask$3.done(ModernAsyncTask.java:137)
at java.util.concurrent.FutureTask$Sync.innerSetException(FutureTask.java:273)
at java.util.concurrent.FutureTask.setException(FutureTask.java:124)
at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:307)
at java.util.concurrent.FutureTask.run(FutureTask.java:137)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1076)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:569)
at java.lang.Thread.run(Thread.java:856)
Caused by: java.lang.IllegalStateException: Cannot perform this operation because the connection pool has been closed.
at android.database.sqlite.SQLiteConnectionPool.throwIfClosedLocked(SQLiteConnectionPool.java:962)
at android.database.sqlite.SQLiteConnectionPool.waitForConnection(SQLiteConnectionPool.java:599)
at android.database.sqlite.SQLiteConnectionPool.acquireConnection(SQLiteConnectionPool.java:348)
at android.database.sqlite.SQLiteSession.acquireConnection(SQLiteSession.java:894)
at android.database.sqlite.SQLiteSession.executeForCursorWindow(SQLiteSession.java:834)
at android.database.sqlite.SQLiteQuery.fillWindow(SQLiteQuery.java:62)
at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:143)
at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:133)
at android.content.ContentResolver.query(ContentResolver.java:388)
at android.content.ContentResolver.query(ContentResolver.java:313)
at android.support.v4.content.CursorLoader.loadInBackground(CursorLoader.java:49)
at android.support.v4.content.CursorLoader.loadInBackground(CursorLoader.java:35)
at android.support.v4.content.AsyncTaskLoader.onLoadInBackground(AsyncTaskLoader.java:240)
at android.support.v4.content.AsyncTaskLoader$LoadTask.doInBackground(AsyncTaskLoader.java:51)
at android.support.v4.content.AsyncTaskLoader$LoadTask.doInBackground(AsyncTaskLoader.java:40)
at android.support.v4.content.ModernAsyncTask$2.call(ModernAsyncTask.java:123)
at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:305)
... 4 more
エラー メッセージからわかったのは、コンテンツ プロバイダーのクエリ メソッドが書き込み可能なデータベースを閉じているということです。コンテンツプロバイダーのスレッドセーフと関係があると思います。スレッド セーフの問題の一時的な回避策として、データベースの書き込み操作 (順次実行) のみを担当するサービス (DatabaseService) を作成しました。Java で複数のスレッドを使用する基本は理解していると思いますが、この特定の問題に知識を適用するには多すぎるので、ここに助けを求めに来ました。
リスト付きのframentの完全なソースは次のとおりです
package cz.muni.fi.WebNovelReader.Fragments;
import android.content.ContentUris;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.SimpleCursorAdapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.ListView;
import com.actionbarsherlock.app.SherlockListFragment;
import cz.muni.fi.WebNovelReader.Library.ReaderActivity;
import cz.muni.fi.WebNovelReader.R;
import cz.muni.fi.WebNovelReader.Tools.Provider;
public class LiveNewestChaptersFragment extends SherlockListFragment implements LoaderManager.LoaderCallbacks<Cursor> {
Uri uri;
ListView list;
SimpleCursorAdapter adapter;
View footer = null;
int offset;
int lastEverVisible = 0;
int count = 0;
boolean loadingMore = false;
String orderBy = Provider.Columns.CHAPTER_PUBLISHED;
String sortOrder = " DESC";
/* **********************
* Fragment Lifecycle *
************************/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.live_servers_fragment, container, false);
list = (ListView)view.findViewById(android.R.id.list);
list.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView absListView, int i) {
//To change body of implemented methods use File | Settings | File Templates.
}
@Override
public void onScroll(AbsListView absListView, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
//what is the bottom item that is visible
int lastInScreen = firstVisibleItem + visibleItemCount;
if (lastEverVisible < lastInScreen)
lastEverVisible = lastInScreen;
//is the bottom item visible & not loading more already ? Load more !
if ((totalItemCount >= 10) && (lastInScreen == totalItemCount) && (lastEverVisible == lastInScreen) && !(loadingMore)){
if (footer != null)
footer.setVisibility(View.VISIBLE);
offset += 10;
loadingMore = true;
count = totalItemCount;
getLoaderManager().restartLoader(0, null, LiveNewestChaptersFragment.this);
}
}
});
footer = inflater.inflate(R.layout.loading_footer, null, false);
footer.setVisibility(View.GONE);
list.addFooterView(footer);
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
uri = getActivity().getIntent().getData();
offset = 0;
getLoaderManager().initLoader(0, null, this);
adapter = new SimpleCursorAdapter(
getActivity(),
R.layout.live_newest_chapters_item,
null,
new String[] {Provider.Columns.CHAPTER_INDEX, Provider.Columns.CHAPTER_TITLE, Provider.Columns.NOVEL_TITLE, Provider.Columns.AUTHOR_NICKNAME},
new int[] {R.id.chapter_index, R.id.chapter_title, R.id.novel_title, R.id.author_nickname},
0
);
setListAdapter(adapter);
getSherlockActivity().getSupportActionBar().setDisplayShowTitleEnabled(true);
getSherlockActivity().getSupportActionBar().setDisplayShowHomeEnabled(true);
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
Intent i = new Intent(getActivity(), ReaderActivity.class);
i.setData(ContentUris.appendId(
Provider.BaseUris.LIVE.buildUpon().path(Provider.UriPaths.CHAPTERS),
id
).build());
startActivity(i);
}
/* **********************************
* LoaderManager.LoaderCallbacks *
************************************/
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
getSherlockActivity().setSupportProgressBarIndeterminateVisibility(true);
return new CursorLoader(getActivity(),
uri,
new String[] {Provider.Columns.BASE_ID, Provider.Columns.CHAPTER_TITLE, Provider.Columns.CHAPTER_INDEX, Provider.Columns.NOVEL_TITLE, Provider.Columns.AUTHOR_NICKNAME},
null,
new String[] {String.valueOf(offset + 10)},
orderBy + " COLLATE LOCALIZED" + sortOrder
);
}
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
if (cursor.getCount() > count) {
if (footer != null)
footer.setVisibility(View.GONE);
adapter.swapCursor(cursor);
loadingMore = false;
}
getSherlockActivity().setSupportProgressBarIndeterminateVisibility(false);
}
@Override
public void onLoaderReset(Loader loader) {
adapter.swapCursor(null);
if (loader.isStarted())
loader.reset();
}
}
簡略化されたクエリ メソッド
public Cursor query(Uri uri, String[] proj, String sel, String[] selectionArgs, String orderBy) {
String selection = sel;
String[] projection;
if ((proj == null) || (proj.length == 0)) {
projection = new String[] {"*"};
}else{
projection = new String[proj.length];
System.arraycopy(proj, 0, projection, 0, proj.length);
}
String table;
String groupBy = null;
String limit = null;
StringBuilder temp;
boolean distinct = false;
switch (uriMatcher.match(uri)) {
// initialize variables needed to make the query
}
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor result = null;
try{
result = db.query(distinct, table, projection, selection, SelectionArgs, groupBy, null, orderBy, limit);
if (result != null) {
result.setNotificationUri(getContext().getContentResolver(), uri);
if ((uriMatcher.match(uri) == URI_MATCHER_CHAPTERS_BY_SERVER_ID) && (ContentUris.parseId(uri) == 0)) {
int offset = Integer.valueOf(limit);
if (lastLiveLimit < offset) {
lastLiveLimit = offset;
DownloaderItemContent download = new DownloaderItemContent(url, insertUri);
Intent i = new Intent(getContext(), DownloaderService.class);
i.putExtra(DownloaderService.EXTRA_DOWNLOAD_ITEM, download);
getContext().startService(i);
}
}
}
}catch (SQLiteException e) {
Log.e("WebNovel Reader", "LibraryProvider: query(): SQLiteException thrown:\n" + e.getMessage());
} catch (MalformedURLException e) {
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
}
return result;
}
簡略化された bulkInsert メソッド
ソースへの注: データを挿入するために bulkInsert メソッドを使用します。チェックを実行するには 3 つのエンティティすべてが必要なので、1 回のメソッド呼び出しで 3 つのエンティティすべてを取得するように ContentValues の配列を構築することにしました。構造は、次のとおりです。著者のコンテンツ値、この著者の小説のコンテンツ値、およびこの小説の章のコンテンツ値が次々と続きます。次のコンテンツの値は、別の著者の小説などです (パターン [author1, Novel1, chapter1, chapter2, Novel2, chapter1, chapter2, author2, Novel1, chapter1])。これは、DFS によってツリー グラフをシリアル化し、それらを次々と配置することに似ています。
private int bulkInsertOrUpdate(Uri uri, ContentValues[] values) {
//basic checking
.
.
.
//loop through authors
while (i < values.length) {
int offset = 0;
List<ContentValues> novels_with_children = new ArrayList<ContentValues>();
List<ContentValues[]> chapters = new ArrayList<ContentValues[]>();
int max1 = 0;
// check if content values of author or identifier of child count are incorrect. If so, skip to the next author
// shift 1 array entry to novel content values
offset++;
// loop through author's novels
for (int j = 0; j < max1; j++) {
int novelIndex = i + offset;
int max2;
// check if content values of novel or identifier of child count are incorrect. If so, skip to the next novel
// shift 1 array entry to chapter content values
offset++;
ContentValues[] temp = new ContentValues[max2];
for (int a = 0; a < max2; a++) {
temp[a] = null;
}
boolean flag_empty = true;
// loop through chapters
for (int k = 0; k < max2; k++) {
// check if content values of chapter or identifier of child count are incorrect. If so, skip to the next chapter
// at least 1 chapter was added, novel is not without chapters
flag_empty = false;
}
// if novel has no chapters skip it
offset += max2;
// if novel is OK, add it for SQLite insert
novels_with_children.add(values[novelIndex]);
// add novel chapters for SQLite insert
chapters.add(temp);
}
// if author has no novels skip him
/****************
* ACTIVE SQL *
****************/
boolean authorInserted = false;
boolean novelInserted = false;
boolean chapterInserted = false;
int count_author_instance = 0;
int count_novel_instance = 0;
serverID = values[i].getAsLong(Columns.AUTHOR_SERVER_ID);
picturesToDownload.clear();
String authorPictureUrl = values[i].getAsString(Columns.AUTHOR_PICTURE);
if (values[i].containsKey(Columns.AUTHOR_PICTURE))
values[i].remove(Columns.AUTHOR_PICTURE);
SQLiteDatabase dbwrite = dbHelper.getWritableDatabase();
SQLiteStatement insertGenresChaptersConnector = dbwrite.compileStatement ("INSERT OR IGNORE INTO " + Tables.GENRES_CHAPTERS_CONNECTOR + "(" + Columns.CHAPTER_ID + ", " + Columns.GENRE_ID + ", " + Columns.NOVEL_ID + ") VALUES (?, ?, ?)");
//transaction author start
(2040) dbwrite.beginTransaction();
try{
// serach for author
Cursor authorIDcursor = dbwrite.query(Tables.AUTHORS, new String[] {Columns.BASE_ID}, "(" + Columns.AUTHOR_NICKNAME + " = ?) AND (" + Columns.AUTHOR_SERVER_ID + " = ?)", new String[]{values[i].getAsString(Columns.AUTHOR_NICKNAME), Integer.toString(values[i].getAsInteger(Columns.AUTHOR_SERVER_ID))}, null, null, null);
long authorID;
boolean flag_transaction2_successful = false;
if (authorIDcursor.moveToFirst()) {
// if found, get his id
authorID = authorIDcursor.getInt(0);
}else{
// create if not found
authorID = dbwrite.insert(Tables.AUTHORS, null, values[i]);
}
authorIDcursor.close();
// for every author's novel
for (int j = 0; j < novels_with_children.size(); j++) {
ContentValues cvn = novels_with_children.get(j);
ContentValues[] cvc = chapters.get(j);
boolean flag_transaction3_successful = false;
//transaction novel start
dbwrite.beginTransaction();
try {
// search for novel
Cursor novelIDcursor = dbwrite.query(Tables.NOVELS, new String[]{Columns.BASE_ID}, "(" + Columns.NOVEL_TITLE + " = ?)", new String[]{cvn.getAsString(Columns.NOVEL_TITLE)}, null, null, null);
long novelID;
if (novelIDcursor.moveToFirst()) {
// if found get its id
novelID = novelIDcursor.getInt(0);
}else{
//create if not found
novelID = dbwrite.insert(Tables.NOVELS, null, cvn);
}
novelIDcursor.close();
// for every chapter
for (ContentValues chapterCV : cvc) {
//skip if chapter did not pass through initial control tests
if (chapterCV == null) {
continue;
}
//find previous chapter if exists
Cursor previous = dbwrite.query(Tables.CHAPTERS, new String[] {Columns.BASE_ID}, "(" + Columns.NOVEL_ID + " = ?) AND (" + Columns.CHAPTER_INDEX + " = ?)", new String[] {String.valueOf(novelID), String.valueOf(chapterIndex - 1)}, null, null, null);
if ((previous != null) && (previous.moveToFirst()))
chapterCV.put(Columns.CHAPTER_ID_PREVIOUS, previous.getLong(previous.getColumnIndex(Columns.BASE_ID)));
previous.close();
//find next chapter if exists
Cursor next = dbwrite.query(Tables.CHAPTERS, new String[] {Columns.BASE_ID}, "(" + Columns.NOVEL_ID + " = ?) AND (" + Columns.CHAPTER_INDEX + " = ?)", new String[] {String.valueOf(novelID), String.valueOf(chapterIndex + 1)}, null, null, null);
if ((next != null) && (next.moveToFirst()))
chapterCV.put(Columns.CHAPTER_ID_NEXT, next.getLong(next.getColumnIndex(Columns.BASE_ID)));
next.close();
//transaction chapter start
dbwrite.beginTransaction();
try {
// search for chapter
Cursor chapterIDcursor = dbwrite.query(Tables.CHAPTERS, new String[]{Columns.BASE_ID}, "(" + Columns.NOVEL_ID + " = ?) AND (" + Columns.CHAPTER_INDEX + " = ?)", new String[]{Long.toString(novelID), chapterCV.getAsInteger(Columns.CHAPTER_INDEX).toString()}, null, null, null);
long chapterID;
if (chapterIDcursor.moveToFirst()) {
// if found get its id
chapterID = chapterIDcursor.getInt(0);
} else {
// create if not found
chapterID = dbwrite.insert(Tables.CHAPTERS, null, chapterCV);
}
chapterIDcursor.close();
// use pre-compiled statement to create connection between genre and chapter (insert entry into connector table)
for (byte genreID : genres) {
insertGenresChaptersConnector.bindLong(1, chapterID);
insertGenresChaptersConnector.bindLong(2, genreID);
insertGenresChaptersConnector.bindLong(3, novelID);
insertGenresChaptersConnector.executeInsert();
}
//tie consequent chapters together
//write this chapter as NEXT in previous chapter if exests
ContentValues cv = new ContentValues();
cv.put(Columns.CHAPTER_ID_PREVIOUS, chapterID);
dbwrite.update(Tables.CHAPTERS, cv, "(" + Columns.NOVEL_ID + " = ?) AND (" + Columns.CHAPTER_INDEX + " = ?)", new String[] {String.valueOf(novelID), String.valueOf(chapterIndex + 1)});//Tables.CHAPTERS, new String[]{Columns.BASE_ID}, "(" + Columns.NOVEL_TITLE + " = ?)", new String[]{novelValues.getAsString(Columns.AUTHOR_NICKNAME)}, null, null, null);
//write this chapter as PREVIOUS in next chapter if exests
cv.clear();
cv.put(Columns.CHAPTER_ID_NEXT, chapterID);
dbwrite.update(Tables.CHAPTERS, cv, "(" + Columns.NOVEL_ID + " = ?) AND (" + Columns.CHAPTER_INDEX + " = ?)", new String[] {String.valueOf(novelID), String.valueOf(chapterIndex - 1)});
//if chapter and connector entries added successfully
dbwrite.setTransactionSuccessful();
flag_transaction3_successful = true;
count_novel_instance++;
} catch (SQLiteException e) {
//TODO Log
} finally {
//transaction chapter end
dbwrite.endTransaction();
}
}
// if at least one chapter of this novel was successfully added
if (flag_transaction3_successful) {
dbwrite.setTransactionSuccessful();
flag_transaction2_successful = true;
count_novel_instance++;
}else{
count_novel_instance = 0;
}
}catch(SQLiteException e) {
count_novel_instance = 0;
//TODO Log
}finally {
//transaction novel end
dbwrite.endTransaction();
}
}
count_author_instance += count_novel_instance;
// if at least one novel of this author was successfully added
if (flag_transaction2_successful) {
dbwrite.setTransactionSuccessful();
count_author_instance++;
}else{
count_author_instance = 0;
}
}catch (SQLiteException e) {
count_author_instance = 0;
//log
}finally {
//transaction1 end
dbwrite.endTransaction();
dbwrite.close();
}
count += count_author_instance;
i += offset;
}
// notify all uris to be notified
return count;
}
前もって感謝します。他の情報が必要な場合は、お知らせください。