Java tutorial
/****************************************************************************** * Copyright (C) 2012, 2013, 2014 * Younghyung Cho. <yhcting77@gmail.com> * All rights reserved. * * This file is part of FeedHive * * This program is licensed under the FreeBSD license * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * The views and conclusions contained in the software and documentation * are those of the authors and should not be interpreted as representing * official policies, either expressed or implied, of the FreeBSD Project. *****************************************************************************/ package free.yhc.feeder; import static free.yhc.feeder.model.Utils.eAssert; import java.util.Calendar; import java.util.HashSet; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.ActivityNotFoundException; import android.content.Intent; import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.provider.MediaStore.MediaColumns; import android.support.v4.app.Fragment; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.ScrollView; import free.yhc.feeder.UiHelper.OnConfirmDialogAction; import free.yhc.feeder.db.ColumnChannel; import free.yhc.feeder.db.DB; import free.yhc.feeder.db.DBPolicy; import free.yhc.feeder.model.BGTask; import free.yhc.feeder.model.BGTaskUpdateChannel; import free.yhc.feeder.model.BaseBGTask; import free.yhc.feeder.model.ContentsManager; import free.yhc.feeder.model.Err; import free.yhc.feeder.model.Feed; import free.yhc.feeder.model.FeederException; import free.yhc.feeder.model.ListenerManager; import free.yhc.feeder.model.RTTask; import free.yhc.feeder.model.UnexpectedExceptionHandler; import free.yhc.feeder.model.Utils; public class ChannelListFragment extends Fragment implements UnexpectedExceptionHandler.TrackedModule { private static final boolean DBG = false; private static final Utils.Logger P = new Utils.Logger(ChannelListFragment.class); private static final int DATA_ARR_MAX = 100; private static final int DATA_REQ_SZ = 20; private static final int REQC_PICK_IMAGE = 0; private static final String KEY_CATID = "categoryid"; private static final String KEY_PRIMARY = "primary"; private final DBPolicy mDbp = DBPolicy.get(); private final RTTask mRtt = RTTask.get(); private DBWatcher mDbWatcher = null; private boolean mPrimary = false; private long mCatId = -1; private ListView mListView = null; // Saved cid for Async execution. private long mCidPickImage = -1; private static class DBWatcher implements ListenerManager.Listener { // NOTE // initial value should be 'true' because we don't know what happened to DB // while this fragment instance DOESN'T exist! private boolean _mChannelTableUpdated = true; private final HashSet<Long> _mUpdatedChannelSet = new HashSet<Long>(); void register() { DBPolicy.get().registerUpdatedListener(this, DB.UpdateType.CHANNEL_TABLE.flag() | DB.UpdateType.CHANNEL_DATA.flag()); } void unregister() { DBPolicy.get().unregisterUpdatedListener(this); } void reset() { _mChannelTableUpdated = false; _mUpdatedChannelSet.clear(); } long[] getUpdatedChannels() { return Utils.convertArrayLongTolong(_mUpdatedChannelSet.toArray(new Long[0])); } boolean isChannelTableUpdated() { return _mChannelTableUpdated; } @Override public void onNotify(Object user, ListenerManager.Type type, Object arg0, Object arg1) { switch ((DB.UpdateType) type) { case CHANNEL_TABLE: _mChannelTableUpdated = true; break; case CHANNEL_DATA: _mUpdatedChannelSet.add((Long) arg0); break; default: eAssert(false); } } } private class AdapterActionListener implements ChannelListAdapter.OnActionListener { @Override public void onUpdateClick(ImageView ibtn, long cid) { onContextBtn_channelUpdate(ibtn, cid); } @Override public void onMoveUpClick(ImageView ibtn, long cid) { ChannelListAdapter adapter = getAdapter(); int pos = getPosition(cid); if (pos < 0) { eAssert(false); return; } if (0 == pos) return; // nothing to do mDbp.updatechannel_switchPosition(adapter.getItemInfo_cid(pos - 1), adapter.getItemInfo_cid(pos)); adapter.switchPos(pos - 1, pos); } @Override public void onMoveDownClick(ImageView ibtn, long cid) { ChannelListAdapter adapter = getAdapter(); int pos = getPosition(cid); int cnt = adapter.getCount(); if (pos >= cnt) { eAssert(false); return; } if (cnt - 1 == pos) return; // nothing to do mDbp.updatechannel_switchPosition(adapter.getItemInfo_cid(pos), adapter.getItemInfo_cid(pos + 1)); adapter.switchPos(pos, pos + 1); } } private class UpdateBGTaskListener extends BaseBGTask.OnEventListener { private long _mCid = -1; UpdateBGTaskListener(long cid) { _mCid = cid; } @Override public void onCancelled(BaseBGTask task, Object param) { eAssert(_mCid >= 0); // See comments at "ItemListActivity.UpdateBGTaskListener.OnPostRun" if (getActivity().isFinishing()) return; // NOTE : refresh??? just 'notifying' is enough? // In current DB policy, sometimes DB may be updated even if updating is cancelled! refreshListItem(_mCid); } @Override public void onPreRun(BaseBGTask task) { // NOTE : refresh??? just 'notifying' is enough? channelRttStateChanged(_mCid); } @Override public void onPostRun(BaseBGTask task, Err result) { // See comments at "ItemListActivity.UpdateBGTaskListener.OnPostRun" if (getActivity().isFinishing()) return; // NOTE : refresh??? just 'notifying' is enough? // It should be 'refresh' due to after successful update, // some channel information in DB may be changed. refreshListItem(_mCid); } } private class DownloadBGTaskListener extends BaseBGTask.OnEventListener { private long _mCid = -1; DownloadBGTaskListener(long cid) { _mCid = cid; } @Override public void onCancelled(BaseBGTask task, Object param) { // See comments at "ItemListActivity.UpdateBGTaskListener.OnPostRun" if (getActivity().isFinishing()) return; if (0 == mRtt.getItemsDownloading(_mCid).length) channelRttStateChanged(_mCid); } @Override public void onPostRun(BaseBGTask task, Err result) { // See comments at "ItemListActivity.UpdateBGTaskListener.OnPostRun" if (getActivity().isFinishing()) return; if (0 == mRtt.getItemsDownloading(_mCid).length) channelRttStateChanged(_mCid); } } private class DeleteChannelWorker extends DiagAsyncTask.Worker { private final long[] _mCids; private long _mNrDelItems = 0; DeleteChannelWorker(long[] cids) { _mCids = cids; } @Override public Err doBackgroundWork(DiagAsyncTask task) { _mNrDelItems = mDbp.deleteChannel(_mCids); return Err.NO_ERR; } @Override public void onPostExecute(DiagAsyncTask task, Err result) { UiHelper.showTextToast(getActivity(), _mNrDelItems + getResources().getString(R.string.channel_deleted_msg)); for (long cid : _mCids) ChannelListFragment.this.getAdapter().removeChannel(cid); dataSetChanged(); ScheduledUpdateService.scheduleNextUpdate(Calendar.getInstance()); } } private class PickIconWorker extends DiagAsyncTask.Worker { private long _mCid = -1; private final Uri _mImageUri; PickIconWorker(Uri uri) { _mImageUri = uri; } @Override public Err doBackgroundWork(DiagAsyncTask task) { String[] filePathColumn = { MediaColumns.DATA }; _mCid = mCidPickImage; Cursor c = getActivity().getContentResolver().query(_mImageUri, filePathColumn, null, null, null); if (!c.moveToFirst()) { c.close(); return Err.GET_MEDIA; } int columnIndex = c.getColumnIndex(filePathColumn[0]); String filePath = c.getString(columnIndex); c.close(); // Make url string from file path Bitmap bm = Utils.decodeImage(filePath, Feed.Channel.ICON_MAX_WIDTH, Feed.Channel.ICON_MAX_HEIGHT); byte[] imageData = Utils.compressBitmap(bm); if (null == imageData) return Err.CODEC_DECODE; if (mCidPickImage < 0) { eAssert(false); return Err.UNKNOWN; // something evil!!! } else { mDbp.updateChannel(mCidPickImage, ColumnChannel.IMAGEBLOB, imageData); mCidPickImage = -1; } return Err.NO_ERR; } @Override public void onPostExecute(DiagAsyncTask task, Err result) { if (Err.NO_ERR == result) getAdapter().notifyChannelIconChanged(_mCid); else UiHelper.showTextToast(getActivity(), result.getMsgId()); } } private class RTTaskRegisterListener implements RTTask.OnRegisterListener { @Override public void onRegister(BGTask task, long cid, RTTask.Action act) { if (RTTask.Action.UPDATE == act) mRtt.bind(cid, RTTask.Action.UPDATE, ChannelListFragment.this, new UpdateBGTaskListener(cid)); } @Override public void onUnregister(BGTask task, long cid, RTTask.Action act) { } } private ChannelListActivity getMyActivity() { return (ChannelListActivity) getActivity(); } private ChannelListAdapter getAdapter() { return (ChannelListAdapter) mListView.getAdapter(); } /** * Notify that dataset for adapter is changed. * All list item will try to rebind their own view. * @param lv */ private void dataSetChanged() { // ((ChannelListAdapter)lv.getAdapter()).clearChangeState(); // Delayed action(notify) may lead to unexpected exception. Why? // Delayed action means, action can be triggered even after this fragment is destroyed // In this case adapter and fragment has invalid reference values and this // may lead to unexpected (ex. "try to use recycled bitmap at ImageView") excpetion. // So, DO NOT USE delayed action here. getAdapter().notifyDataSetChanged(); } /** * Notify that dataset of given 'cid' in adapter is changed. * List item of only given 'cid' - one list item - will try to rebind it's view. * @param lv ListView * @param cid */ private void dataSetChanged(long cid) { /* ChannelListAdapter cla = (ChannelListAdapter)lv.getAdapter(); ((ChannelListAdapter)lv.getAdapter()).clearChangeState(); for (int i = lv.getFirstVisiblePosition(); i <= lv.getLastVisiblePosition(); i++) { long itemCid = cla.getChannelId(i); if (itemCid == cid) cla.addChanged(cla.getItemId(i)); else cla.addUnchanged(cla.getItemId(i)); } */ dataSetChanged(); } private void channelRttStateChanged(long cid) { getAdapter().notifyChannelRttStateChanged(cid); } private int getPosition(long cid) { ChannelListAdapter adapter = getAdapter(); for (int i = 0; i < adapter.getCount(); i++) { if (adapter.getItemInfo_cid(i) == cid) return i; } return -1; } /** * Delete channel and it's items from DB. * This completely deletes all channel and items. * @param tab * @param cid */ private void deleteChannel(long cid) { DiagAsyncTask task = new DiagAsyncTask(getActivity(), new DeleteChannelWorker(new long[] { cid }), DiagAsyncTask.Style.SPIN, R.string.deleting_channel_msg); task.run(); } private boolean changeCategory(long cid, long catTo) { mDbp.updateChannel(cid, ColumnChannel.CATEGORYID, catTo); getAdapter().removeItem(getAdapter().findPosition(cid)); getMyActivity().categoryDataSetChanged(catTo); dataSetChanged(); return true; } private void onContextBtn_channelUpdate(ImageView ibtn, long cid) { RTTask.TaskState state = mRtt.getState(cid, RTTask.Action.UPDATE); switch (state) { case IDLE: { BGTaskUpdateChannel task = new BGTaskUpdateChannel(new BGTaskUpdateChannel.Arg(cid)); mRtt.register(cid, RTTask.Action.UPDATE, task); mRtt.start(cid, RTTask.Action.UPDATE); channelRttStateChanged(cid); } break; case RUNNING: case READY: mRtt.cancel(cid, RTTask.Action.UPDATE, null); // to change icon into "canceling" channelRttStateChanged(cid); break; case FAILED: { Err result = mRtt.getErr(cid, RTTask.Action.UPDATE); UiHelper.showTextToast(getActivity(), result.getMsgId()); mRtt.consumeResult(cid, RTTask.Action.UPDATE); channelRttStateChanged(cid); } break; case CANCELING: UiHelper.showTextToast(getActivity(), R.string.wait_cancel); break; default: eAssert(false); } } private void onContext_deleteChannel(final long cid) { OnConfirmDialogAction action = new OnConfirmDialogAction() { @Override public void onOk(Dialog dialog) { deleteChannel(cid); } @Override public void onCancel(Dialog dialog) { } }; UiHelper.buildConfirmDialog(getActivity(), R.string.delete_channel, R.string.delete_channel_msg, action) .show(); } private void onContext_deleteDownloaded(final long cid) { OnConfirmDialogAction action = new OnConfirmDialogAction() { @Override public void onOk(Dialog dialog) { // delete entire channel directory and re-make it. // Why? // All and only downloadded files are located in channel directory. ContentsManager.get().cleanChannelDir(cid); } @Override public void onCancel(Dialog dialog) { } }; UiHelper.buildConfirmDialog(getActivity(), R.string.delete_downloadded_file, R.string.delete_channel_downloadded_file_msg, action).show(); } private void onContext_deleteUsedDownloaded(final long cid) { AlertDialog diag = UiHelper.buildDeleteUsedDnFilesConfirmDialog(cid, this.getActivity(), null, null); if (null == diag) UiHelper.showTextToast(this.getActivity(), R.string.del_dnfiles_not_allowed_msg); diag.show(); } private void onContext_changeCategory(final long cid) { UiHelper.OnCategorySelectedListener action = new UiHelper.OnCategorySelectedListener() { @Override public void onSelected(long category, Object user) { changeCategory(cid, category); } }; UiHelper.selectCategoryDialog(getActivity(), R.string.select_category, action, mCatId, null).show(); } private void onContext_setting(final long cid) { Intent intent = new Intent(getActivity(), ChannelSettingActivity.class); intent.putExtra("cid", cid); startActivity(intent); } private void onContext_pickIcon(final long cid) { Intent i = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI); mCidPickImage = cid; try { startActivityForResult(Intent.createChooser(i, getResources().getText(R.string.pick_icon)), REQC_PICK_IMAGE); } catch (ActivityNotFoundException e) { UiHelper.showTextToast(getActivity(), R.string.warn_find_gallery_app); return; } } private void refreshListItem(long[] cids) { ChannelListAdapter adapter = getAdapter(); Cursor newCursor = ChannelListAdapter.getQueryCursor(mCatId); adapter.changeCursor(newCursor); int[] ids = new int[cids.length]; for (int i = 0; i < ids.length; i++) ids[i] = adapter.findItemId(cids[i]); adapter.reloadItem(ids); dataSetChanged(); } private void refreshListItem(long cid) { refreshListItem(new long[] { cid }); } /** * Cursor is changed and all view should be rebinded. * @param tab */ public void refreshListAsync() { Cursor newCursor = ChannelListAdapter.getQueryCursor(mCatId); getAdapter().changeCursor(newCursor); getAdapter().reloadDataSetAsync(); } public void addChannel(String url, String iconurl) { eAssert(url != null); url = Utils.removeTrailingSlash(url); long cid = -1; try { cid = mDbp.insertNewChannel(getCategoryId(), url); } catch (FeederException e) { UiHelper.showTextToast(getActivity(), e.getError().getMsgId()); return; } // full update for this newly inserted channel BGTaskUpdateChannel task; if (Utils.isValidValue(iconurl)) task = new BGTaskUpdateChannel(new BGTaskUpdateChannel.Arg(cid, iconurl)); else task = new BGTaskUpdateChannel(new BGTaskUpdateChannel.Arg(cid)); mRtt.register(cid, RTTask.Action.UPDATE, task); mRtt.start(cid, RTTask.Action.UPDATE); getAdapter().appendChannel(cid); dataSetChanged(); ScheduledUpdateService.scheduleNextUpdate(Calendar.getInstance()); } public void setToPrimary(boolean primary) { mPrimary = primary; if (null != getActivity() && null != mListView) { if (primary) getActivity().registerForContextMenu(mListView); else getActivity().unregisterForContextMenu(mListView); } } public void onResult_pickImage(int resultCode, Intent data) { if (Activity.RESULT_OK != resultCode) return; // this may takes quite long time (if image size is big!). // So, let's do it in background. new DiagAsyncTask(getActivity(), new PickIconWorker(data.getData()), DiagAsyncTask.Style.SPIN, R.string.pick_icon_progress).run(); } public long getCategoryId() { return mCatId; } @Override public boolean onContextItemSelected(MenuItem mItem) { if (!mPrimary || !getMyActivity().isContextMenuOwner(this)) return false; AdapterContextMenuInfo info = (AdapterContextMenuInfo) mItem.getMenuInfo(); long dbId = getAdapter().getItemInfo_cid(info.position); switch (mItem.getItemId()) { case R.id.delete: onContext_deleteChannel(dbId); return true; case R.id.delete_dnfile: onContext_deleteDownloaded(dbId); return true; case R.id.delete_used_dnfile: onContext_deleteUsedDownloaded(dbId); return true; case R.id.change_category: onContext_changeCategory(dbId); return true; case R.id.setting: onContext_setting(dbId); return true; case R.id.pick_icon: onContext_pickIcon(dbId); return true; } return false; } public void onCreateContextMenu2(ContextMenu menu, View v, ContextMenuInfo menuInfo) { eAssert(mPrimary); MenuInflater inflater = getActivity().getMenuInflater(); inflater.inflate(R.menu.channel_context, menu); AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; if (getAdapter().isLoadingItem(info.position)) return; long dbId = getAdapter().getItemInfo_cid(info.position); RTTask.TaskState updateState = mRtt.getState(dbId, RTTask.Action.UPDATE); if (RTTask.TaskState.RUNNING == updateState || RTTask.TaskState.READY == updateState || RTTask.TaskState.CANCELING == updateState) { menu.findItem(R.id.delete).setEnabled(false); menu.findItem(R.id.pick_icon).setEnabled(false); /* full update is useless at this moment. Codes are left for history tracking menu.findItem(R.id.full_update).setEnabled(false); */ } if (mRtt.getItemsDownloading(dbId).length > 0) { menu.findItem(R.id.delete).setEnabled(false); menu.findItem(R.id.delete_dnfile).setEnabled(false); } } @Override public String dump(UnexpectedExceptionHandler.DumpLevel lv) { return "[ ChannelListFragment ]"; } /* @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); } */ @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case REQC_PICK_IMAGE: onResult_pickImage(resultCode, data); break; } } public static ChannelListFragment newInstance(long catId) { ChannelListFragment f = new ChannelListFragment(); f.initialize(catId); return f; } public void initialize(long catId) { mCatId = catId; } @Override public void onSaveInstanceState(Bundle outState) { outState.putLong(KEY_CATID, mCatId); outState.putBoolean(KEY_PRIMARY, mPrimary); } private void restoreInstanceState(Bundle data) { if (null == data) return; mCatId = data.getLong(KEY_CATID, mCatId); mPrimary = data.getBoolean(KEY_PRIMARY, mPrimary); } @Override public void onViewStateRestored(Bundle savedInstanceState) { super.onViewStateRestored(savedInstanceState); } @Override public void onAttach(Activity activity) { super.onAttach(activity); } @Override public void onCreate(Bundle savedInstanceState) { UnexpectedExceptionHandler.get().registerModule(this); super.onCreate(savedInstanceState); restoreInstanceState(savedInstanceState); mDbWatcher = new DBWatcher(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); LinearLayout ll = (LinearLayout) inflater.inflate(R.layout.channel_listview, null); mListView = (ListView) ll.findViewById(R.id.list); eAssert(null != mListView); mListView.setAdapter(new ChannelListAdapter(getActivity(), ChannelListAdapter.getQueryCursor(mCatId), mListView, DATA_REQ_SZ, DATA_ARR_MAX, new AdapterActionListener())); mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long itemId) { if (getAdapter().isLoadingItem(position)) return; Intent intent = new Intent(getActivity(), ItemListActivity.class); intent.putExtra(ItemListActivity.IKEY_MODE, ItemListActivity.MODE_CHANNEL); intent.putExtra(ItemListActivity.IKEY_FILTER, ItemListActivity.FILTER_NONE); intent.putExtra("cid", ((ChannelListAdapter) parent.getAdapter()).getItemInfo_cid(position)); startActivity(intent); } }); ScrollView sv = (ScrollView) ll.findViewById(R.id.empty_list); mListView.setEmptyView(sv); setToPrimary(mPrimary); return ll; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); } @Override public void onStart() { super.onStart(); if (DBG) P.v("Enter"); } @Override public void onResume() { super.onResume(); //logI("==> ChannelListActivity : onResume"); // NOTE // Case to think about // - new update task is registered between 'registerManagerEventListener' and 'getUpdateState' // - then, this task will be binded twice. // => This leads to over head operation (ex. refreshing list two times continuously etc.) // But, this doesn't issue logical error. So, I can get along with this case. // // If 'registerManagerEventListener' is below 'getUpdateState', // we may miss binding some updating task! mRtt.registerRegisterEventListener(this, new RTTaskRegisterListener()); HashSet<Long> updatedCids = new HashSet<Long>(); for (long cid : mDbWatcher.getUpdatedChannels()) updatedCids.add(cid); HashSet<Long> myUpdatedCids = new HashSet<Long>(); // Check channel state and bind it. // Why here? Not 'onStart'. // See comments in 'onPause()' try { mDbp.getDelayedChannelUpdate(); Cursor c = mDbp.queryChannel(mCatId, ColumnChannel.ID); if (c.moveToFirst()) { do { long cid = c.getLong(0); if (updatedCids.contains(cid)) myUpdatedCids.add(cid); if (RTTask.TaskState.IDLE != mRtt.getState(cid, RTTask.Action.UPDATE)) mRtt.bind(cid, RTTask.Action.UPDATE, this, new UpdateBGTaskListener(cid)); long[] ids = mRtt.getItemsDownloading(cid); for (long id : ids) mRtt.bind(id, RTTask.Action.DOWNLOAD, this, new DownloadBGTaskListener(cid)); } while (c.moveToNext()); } c.close(); } finally { mDbp.putDelayedChannelUpdate(); } // NOTE // Channel may be added or deleted. // And channel operation is only allowed on current selected list // according to use case. if (mDbWatcher.isChannelTableUpdated() && myUpdatedCids.size() > 0) { refreshListAsync(); } else refreshListItem(Utils.convertArrayLongTolong(myUpdatedCids.toArray(new Long[0]))); // We don't need to worry about item table change. // Because, if item is newly inserted, that means some of channel is updated. // And that channel will be updated according to DB changes. mDbWatcher.unregister(); mDbWatcher.reset(); } @Override public void onPause() { //logI("==> ChannelListActivity : onPause"); mDbWatcher.register(); mRtt.unregisterRegisterEventListener(this); // Why This should be here (NOT 'onStop'!) // In normal case, starting 'ItemListAcvitiy' issues 'onStop'. // And when exiting from 'ItemListActivity' by back-key event, 'onStart' is called. // But, during updating - there is background thread - 'onResume' and 'onCancel' are called // instead of 'onStart' and 'onStop'. // That is, if there is background running background thread, activity is NOT stopped but just paused. // (This is experimental conclusion - NOT by analyzing framework source code.) // I think this is Android's bug or implicit policy. // Because of above issue, 'binding' and 'unbinding' are done at 'onResume' and 'onPause'. mRtt.unbind(this); super.onPause(); } @Override public void onStop() { if (DBG) P.v("Enter"); super.onStop(); } @Override public void onDestroyView() { super.onDestroyView(); } @Override public void onDestroy() { super.onDestroy(); mDbWatcher.unregister(); UnexpectedExceptionHandler.get().unregisterModule(this); } @Override public void onDetach() { super.onDetach(); } }