/* * Copyright (C) 2009 Muthu Ramadoss. All rights reserved. * * Modified from Romain Guy Shelves project to suit Books-Exchange requirements. * Original source from Shelves - http://code.google.com/p/shelves/ */ /* * Copyright (C) 2008 Romain Guy * * 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.androidrocks.bex.activity; import android.app.Activity; import android.app.SearchManager; import android.content.ActivityNotFoundException; import android.content.ContentResolver; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Config; import android.view.ContextMenu; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewStub; import android.view.Gravity; import android.view.ViewGroup; import android.view.animation.AnimationUtils; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.PopupWindow; import com.androidrocks.bex.drawable.CrossFadeDrawable; import com.androidrocks.bex.drawable.FastBitmapDrawable; import com.androidrocks.bex.provider.BookStoreFactory; import com.androidrocks.bex.provider.BooksManager; import com.androidrocks.bex.provider.BooksStore; import com.androidrocks.bex.provider.BooksUpdater; import com.androidrocks.bex.scan.ScanIntent; import com.androidrocks.bex.util.ImageUtilities; import com.androidrocks.bex.util.ImportUtilities; import com.androidrocks.bex.util.UIUtilities; import com.androidrocks.bex.util.UserTask; import com.androidrocks.bex.view.ShelvesView; import com.androidrocks.bex.R; import java.io.IOException; import java.util.List; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicInteger; public class ShelvesActivity extends Activity { private static final String LOG_TAG = "Shelves"; private static final int REQUEST_SCAN_FOR_ADD = 1; private static final int REQUEST_SCAN_FOR_CHECK = 2; private static final int COVER_TRANSITION_DURATION = 175; private static final int MESSAGE_UPDATE_BOOK_COVERS = 1; private static final int DELAY_SHOW_BOOK_COVERS = 550; private static final int WINDOW_DISMISS_DELAY = 600; private static final int WINDOW_SHOW_DELAY = 600; private static final String ACTION_IMPORT = "shelves.intent.action.ACTION_IMPORT"; private static final String STATE_IMPORT_IN_PROGRESS = "shelves.import.inprogress"; private static final String STATE_IMPORT_BOOKS = "shelves.import.books"; private static final String STATE_IMPORT_INDEX = "shelves.import.index"; private static final String STATE_ADD_IN_PROGRESS = "shelves.add.inprogress"; private static final String STATE_ADD_BOOK = "shelves.add.book"; private ImportTask mImportTask; private AddTask mAddTask; private BooksUpdater mBooksUpdater; private final Handler mScrollHandler = new ScrollHandler(); private int mScrollState = ShelvesScrollManager.SCROLL_STATE_IDLE; private boolean mPendingCoversUpdate; private boolean mFingerUp = true; private PopupWindow mPopup; private FastBitmapDrawable mDefaultCover; private View mGridPosition; private TextView mGridPositionText; private ProgressBar mImportProgress; private View mImportPanel; private View mAddPanel; private ShelvesView mGrid; private Bundle mSavedState; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.screen_shelves); getWindow().setBackgroundDrawable(null); mBooksUpdater = new BooksUpdater(this); setupViews(); handleSearchQuery(getIntent()); } int getScrollState() { return mScrollState; } boolean isPendingCoversUpdate() { return mPendingCoversUpdate; } FastBitmapDrawable getDefaultCover() { return mDefaultCover; } private void handleSearchQuery(Intent queryIntent) { final String queryAction = queryIntent.getAction(); if (Intent.ACTION_SEARCH.equals(queryAction)) { onSearch(queryIntent); } else if (Intent.ACTION_VIEW.equals(queryAction)) { final Intent viewIntent = new Intent(Intent.ACTION_VIEW, queryIntent.getData()); startActivity(viewIntent); } } private void onSearch(Intent intent) { final String queryString = intent.getStringExtra(SearchManager.QUERY); mGrid.setFilterText(queryString); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); final String action = intent.getAction(); if (Intent.ACTION_SEARCH.equals(action)) { onSearch(intent); } else if (Intent.ACTION_VIEW.equals(action)) { final Intent viewIntent = new Intent(Intent.ACTION_VIEW, intent.getData()); startActivity(viewIntent); } else if (ACTION_IMPORT.equals(action)) { onImport(); } } private void setupViews() { final BooksAdapter adapter = new BooksAdapter(this); mDefaultCover = adapter.getDefaultCover(); mGrid = (ShelvesView) findViewById(R.id.grid_shelves); final ShelvesView grid = mGrid; grid.setTextFilterEnabled(true); grid.setAdapter(adapter); grid.setOnScrollListener(new ShelvesScrollManager()); grid.setOnTouchListener(new FingerTracker()); grid.setOnItemSelectedListener(new SelectionTracker()); grid.setOnItemClickListener(new BookViewer()); registerForContextMenu(grid); mGridPosition = getLayoutInflater().inflate(R.layout.grid_position, null); mGridPositionText = (TextView) mGridPosition.findViewById(R.id.text); } @Override protected void onResume() { super.onResume(); mBooksUpdater.start(); if (mSavedState != null) restoreLocalState(mSavedState); } @Override protected void onPause() { super.onPause(); stopBooksUpdater(); } @Override protected void onStop() { super.onStop(); stopBooksUpdater(); } @Override protected void onDestroy() { super.onDestroy(); dismissPopup(); stopBooksUpdater(); onCancelAdd(); onCancelImport(); ImageUtilities.cleanupCache(); } private void stopBooksUpdater() { final BooksUpdater booksUpdater = mBooksUpdater; booksUpdater.clear(); booksUpdater.stop(); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); restoreLocalState(savedInstanceState); mSavedState = null; } private void restoreLocalState(Bundle savedInstanceState) { restoreAddTask(savedInstanceState); restoreImportTask(savedInstanceState); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); saveAddTask(outState); saveImportTask(outState); mSavedState = outState; } private void saveAddTask(Bundle outState) { final AddTask task = mAddTask; if (task != null && task.getStatus() != UserTask.Status.FINISHED) { final String bookId = task.getBookId(); task.cancel(true); if (bookId != null) { outState.putBoolean(STATE_ADD_IN_PROGRESS, true); outState.putString(STATE_ADD_BOOK, bookId); } mAddTask = null; } } private void restoreAddTask(Bundle savedInstanceState) { if (savedInstanceState.getBoolean(STATE_ADD_IN_PROGRESS)) { final String id = savedInstanceState.getString(STATE_ADD_BOOK); if (!BooksManager.bookExists(getContentResolver(), id)) { mAddTask = (AddTask) new AddTask().execute(id); } } } private void saveImportTask(Bundle outState) { final ImportTask task = mImportTask; if (task != null && task.getStatus() != UserTask.Status.FINISHED) { task.cancel(true); outState.putBoolean(STATE_IMPORT_IN_PROGRESS, true); outState.putStringArrayList(STATE_IMPORT_BOOKS, task.mBooks); outState.putInt(STATE_IMPORT_INDEX, task.mImportCount.get()); mImportTask = null; } } private void restoreImportTask(Bundle savedInstanceState) { if (savedInstanceState.getBoolean(STATE_IMPORT_IN_PROGRESS)) { ArrayList<String> books = savedInstanceState.getStringArrayList(STATE_IMPORT_BOOKS); int index = savedInstanceState.getInt(STATE_IMPORT_INDEX); if (books != null) { if (index < books.size()) { mImportTask = (ImportTask) new ImportTask(books, index).execute(); } } else { mImportTask = (ImportTask) new ImportTask().execute(); } } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.shelves, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onMenuItemSelected(int featureId, MenuItem item) { switch (item.getItemId()) { case R.id.menu_item_add_search: onAddSearch(); return true; case R.id.menu_item_add: onAdd(); return true; case R.id.menu_item_check: onCheck(); return true; case R.id.menu_item_search: onSearchRequested(); return true; case R.id.menu_item_settings: onSettings(); return true; } return super.onMenuItemSelected(featureId, item); } private void onSettings() { SettingsActivity.show(this); } @Override public boolean onPrepareOptionsMenu(Menu menu) { final boolean scanAvailable = ScanIntent.isInstalled(this); MenuItem item; item = menu.findItem(R.id.menu_item_add); item.setEnabled(scanAvailable && (mAddTask == null || mAddTask.getStatus() == UserTask.Status.FINISHED)); item = menu.findItem(R.id.menu_item_check); item.setEnabled(scanAvailable); return super.onPrepareOptionsMenu(menu); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_SEARCH) { return onSearchRequested(); } return super.onKeyUp(keyCode, event); } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; menu.setHeaderTitle(((TextView) info.targetView).getText()); getMenuInflater().inflate(R.menu.book, menu); } @Override public boolean onContextItemSelected(MenuItem item) { final AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); final BookViewHolder holder = (BookViewHolder) info.targetView.getTag(); switch (item.getItemId()) { case R.id.context_menu_item_view: onView(holder.bookId); return true; case R.id.context_menu_item_buy: onBuy(BooksManager.findBook(getContentResolver(), holder.bookId)); return true; case R.id.context_menu_item_delete: onDelete(holder.bookId); return true; } return super.onContextItemSelected(item); } private void onView(String bookId) { BookDetailsActivity.show(this, bookId); } private void onBuy(BooksStore.Book book) { final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(book.getDetailsUrl())); startActivity(intent); } private void onDelete(String bookId) { if (BooksManager.deleteBook(getContentResolver(), bookId)) { UIUtilities.showToast(this, R.string.success_book_deleted); } } private void startScan(int code) { try { final Intent intent = new Intent(ScanIntent.INTENT_ACTION_SCAN); intent.putExtra(ScanIntent.INTENT_EXTRA_SCAN_MODE, ScanIntent.INTENT_EXTRA_PRODUCT_MODE); startActivityForResult(intent, code); } catch (ActivityNotFoundException e) { UIUtilities.showToast(this, R.string.error_missing_barcode_scanner, true); } } private void onAddSearch() { AddBookActivity.show(this); } private void onAdd() { startScan(REQUEST_SCAN_FOR_ADD); } private void onCheck() { startScan(REQUEST_SCAN_FOR_CHECK); } private void onImport() { if (mImportTask == null || mImportTask.getStatus() == ImportTask.Status.FINISHED) { mImportTask = (ImportTask) new ImportTask().execute(); } else { UIUtilities.showToast(this, R.string.error_import_in_progress); } } private void onCancelAdd() { if (mAddTask != null && mAddTask.getStatus() == UserTask.Status.RUNNING) { mAddTask.cancel(true); mAddTask = null; } } private void onCancelImport() { if (mImportTask != null && mImportTask.getStatus() == UserTask.Status.RUNNING) { mImportTask.cancel(true); mImportTask = null; } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK) { switch (requestCode) { case REQUEST_SCAN_FOR_ADD: onScanAdd(data); break; case REQUEST_SCAN_FOR_CHECK: onScanCheck(data); break; } } } private void onScanAdd(Intent data) { final Bundle bundle = data.getExtras(); if (ScanIntent.FORMAT_EAN_13.equals(bundle.getString(ScanIntent.SCAN_RESULT_FORMAT))) { final String id = bundle.getString(ScanIntent.SCAN_RESULT); if (!BooksManager.bookExists(getContentResolver(), id)) { mAddTask = (AddTask) new AddTask().execute(id); } else { UIUtilities.showToast(this, R.string.error_book_exists); } } } private void onScanCheck(Intent data) { final Bundle bundle = data.getExtras(); if (ScanIntent.FORMAT_EAN_13.equals(bundle.getString(ScanIntent.SCAN_RESULT_FORMAT))) { final String id = bundle.getString(ScanIntent.SCAN_RESULT); final String bookId = BooksManager.findBookId(getContentResolver(), id); if (bookId == null) { UIUtilities.showImageToast(this, R.string.success_book_not_found, getResources().getDrawable(R.drawable.unknown_book)); } else { UIUtilities.showImageToast(this, R.string.error_book_exists, ImageUtilities.getCachedCover(bookId, mDefaultCover)); } } } private void showPanel(View panel, boolean slideUp) { panel.startAnimation(AnimationUtils.loadAnimation(this, slideUp ? R.anim.slide_in : R.anim.slide_out_top)); panel.setVisibility(View.VISIBLE); } private void hidePanel(View panel, boolean slideDown) { panel.startAnimation(AnimationUtils.loadAnimation(this, slideDown ? R.anim.slide_out : R.anim.slide_in_top)); panel.setVisibility(View.GONE); } private void updateBookCovers() { mPendingCoversUpdate = false; final ShelvesView grid = mGrid; final FastBitmapDrawable cover = mDefaultCover; final int count = grid.getChildCount(); for (int i = 0; i < count; i++) { final View view = grid.getChildAt(i); final BookViewHolder holder = (BookViewHolder) view.getTag(); if (holder.queryCover) { final String bookId = holder.bookId; FastBitmapDrawable cached = ImageUtilities.getCachedCover(bookId, cover); CrossFadeDrawable d = holder.transition; d.setEnd(cached.getBitmap()); holder.title.setCompoundDrawablesWithIntrinsicBounds(null, null, null, d); d.startTransition(COVER_TRANSITION_DURATION); holder.queryCover = false; } } grid.invalidate(); } private void postUpdateBookCovers() { Handler handler = mScrollHandler; Message message = handler.obtainMessage(MESSAGE_UPDATE_BOOK_COVERS, ShelvesActivity.this); handler.removeMessages(MESSAGE_UPDATE_BOOK_COVERS); mPendingCoversUpdate = true; handler.sendMessage(message); } private void dismissPopup() { if (mPopup != null) { mPopup.dismiss(); } } private void showPopup() { if (mPopup == null) { PopupWindow p = new PopupWindow(this); p.setFocusable(false); p.setContentView(mGridPosition); p.setWidth(ViewGroup.LayoutParams.FILL_PARENT); p.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); p.setBackgroundDrawable(null); p.setAnimationStyle(R.style.PopupAnimation); mPopup = p; } if (mGrid.getWindowVisibility() == View.VISIBLE) { mPopup.showAtLocation(mGrid, Gravity.CENTER, 0, 0); } } private class AddTask extends UserTask<String, Void, BooksStore.Book> { private final Object mLock = new Object(); private String mBookId; @Override public void onPreExecute() { if (mAddPanel == null) { mAddPanel = ((ViewStub) findViewById(R.id.stub_add)).inflate(); ((ProgressBar) mAddPanel.findViewById(R.id.progress)).setIndeterminate(true); ((TextView) mAddPanel.findViewById(R.id.label_import)).setText( getText(R.string.add_label)); final View cancelButton = mAddPanel.findViewById(R.id.button_cancel); cancelButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { onCancelAdd(); } }); } showPanel(mAddPanel, false); } String getBookId() { synchronized (mLock) { return mBookId; } } public BooksStore.Book doInBackground(String... params) { synchronized (mLock) { mBookId = params[0]; } return BooksManager.loadAndAddBook(getContentResolver(), mBookId, BookStoreFactory.get(ShelvesActivity.this)); } @Override public void onCancelled() { hidePanel(mAddPanel, false); } @Override public void onPostExecute(BooksStore.Book book) { if (book == null) { UIUtilities.showToast(ShelvesActivity.this, R.string.error_adding_book); } else { UIUtilities.showFormattedImageToast(ShelvesActivity.this, R.string.success_added, ImageUtilities.getCachedCover(book.getInternalId(), mDefaultCover), book.getTitle()); } hidePanel(mAddPanel, false); } } private class ImportTask extends UserTask<Void, Integer, Integer> { private ContentResolver mResolver; final AtomicInteger mImportCount = new AtomicInteger(); ArrayList<String> mBooks; ImportTask() { } ImportTask(ArrayList<String> books, int index) { mBooks = books; mImportCount.set(index); } @Override public void onPreExecute() { if (mImportPanel == null) { mImportPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate(); mImportProgress = (ProgressBar) mImportPanel.findViewById(R.id.progress); final View cancelButton = mImportPanel.findViewById(R.id.button_cancel); cancelButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { onCancelImport(); } }); } mResolver = getContentResolver(); mImportProgress.setProgress(0); showPanel(mImportPanel, true); } public Integer doInBackground(Void... params) { int imported = 0; try { if (mBooks == null) mBooks = ImportUtilities.loadItems(); final List<String> list = mBooks; final BooksStore booksStore = BookStoreFactory.get(ShelvesActivity.this); final int count = list.size(); final ContentResolver resolver = mResolver; final AtomicInteger importCount = mImportCount; for (int i = importCount.get(); i < count; i++) { publishProgress(i, count); if (isCancelled()) return null; final String id = list.get(i); if (!BooksManager.bookExists(mResolver, id)) { if (isCancelled()) return null; BooksStore.Book book = BooksManager.loadAndAddBook(resolver, id, booksStore); if (book != null) { if (Config.LOGD) { android.util.Log.d(LOG_TAG, book.toString()); } imported++; } } importCount.incrementAndGet(); } } catch (IOException e) { return null; } return imported; } @Override public void onProgressUpdate(Integer... values) { final ProgressBar progress = mImportProgress; progress.setMax(values[1]); progress.setProgress(values[0]); } @Override public void onCancelled() { hidePanel(mImportPanel, true); } @Override public void onPostExecute(Integer countImport) { if (countImport == null) { UIUtilities.showToast(ShelvesActivity.this, R.string.error_missing_import_file); } else { UIUtilities.showFormattedToast(ShelvesActivity.this, R.string.success_imported, countImport); } hidePanel(mImportPanel, true); } } private class ShelvesScrollManager implements AbsListView.OnScrollListener { private String mPreviousPrefix; private boolean mPopupWillShow; private final Runnable mShowPopup = new Runnable() { public void run() { showPopup(); } }; private final Runnable mDismissPopup = new Runnable() { public void run() { mScrollHandler.removeCallbacks(mShowPopup); mPopupWillShow = false; dismissPopup(); } }; public void onScrollStateChanged(AbsListView view, int scrollState) { if (mScrollState == SCROLL_STATE_FLING && scrollState != SCROLL_STATE_FLING) { final Handler handler = mScrollHandler; final Message message = handler.obtainMessage(MESSAGE_UPDATE_BOOK_COVERS, ShelvesActivity.this); handler.removeMessages(MESSAGE_UPDATE_BOOK_COVERS); handler.sendMessageDelayed(message, mFingerUp ? 0 : DELAY_SHOW_BOOK_COVERS); mPendingCoversUpdate = true; } else if (scrollState == SCROLL_STATE_FLING) { mPendingCoversUpdate = false; mScrollHandler.removeMessages(MESSAGE_UPDATE_BOOK_COVERS); } if (scrollState == SCROLL_STATE_IDLE) { mScrollHandler.removeCallbacks(mShowPopup); final BooksUpdater booksUpdater = mBooksUpdater; final int count = view.getChildCount(); for (int i = 0; i < count; i++) { booksUpdater.offer(((BookViewHolder) view.getChildAt(i).getTag()).bookId); } } else { mBooksUpdater.clear(); } mScrollState = scrollState; } public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { if (mScrollState != SCROLL_STATE_FLING) return; final int count = view.getChildCount(); if (count == 0) return; final StringBuilder buffer = new StringBuilder(7); String title = ((BookViewHolder) view.getChildAt(0).getTag()).sortTitle; title = title.substring(0, Math.min(title.length(), 2)); if (title.length() == 2) { buffer.append(Character.toUpperCase(title.charAt(0))); buffer.append(title.charAt(1)); } else { buffer.append(title.toUpperCase()); } if (count > 1) { buffer.append(" - "); final int lastChild = count - 1; title = ((BookViewHolder) view.getChildAt(lastChild).getTag()).sortTitle; title = title.substring(0, Math.min(title.length(), 2)); if (title.length() == 2) { buffer.append(Character.toUpperCase(title.charAt(0))); buffer.append(title.charAt(1)); } else { buffer.append(title.toUpperCase()); } } final String prefix = buffer.toString(); final Handler scrollHandler = mScrollHandler; if (!mPopupWillShow && (mPopup == null || !mPopup.isShowing()) && !prefix.equals(mPreviousPrefix)) { mPopupWillShow = true; final Runnable showPopup = mShowPopup; scrollHandler.removeCallbacks(showPopup); scrollHandler.postDelayed(showPopup, WINDOW_SHOW_DELAY); } mGridPositionText.setText(prefix); mPreviousPrefix = prefix; final Runnable dismissPopup = mDismissPopup; scrollHandler.removeCallbacks(dismissPopup); scrollHandler.postDelayed(dismissPopup, WINDOW_DISMISS_DELAY); } } private static class ScrollHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_UPDATE_BOOK_COVERS: ((ShelvesActivity) msg.obj).updateBookCovers(); break; } } } private class FingerTracker implements View.OnTouchListener { public boolean onTouch(View view, MotionEvent event) { final int action = event.getAction(); mFingerUp = action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL; if (mFingerUp && mScrollState != ShelvesScrollManager.SCROLL_STATE_FLING) { postUpdateBookCovers(); } return false; } } private class SelectionTracker implements AdapterView.OnItemSelectedListener { public void onItemSelected(AdapterView<?> adapterView, View view, int position, long id) { if (mScrollState != ShelvesScrollManager.SCROLL_STATE_IDLE) { mScrollState = ShelvesScrollManager.SCROLL_STATE_IDLE; postUpdateBookCovers(); } } public void onNothingSelected(AdapterView<?> adapterView) { } } private class BookViewer implements AdapterView.OnItemClickListener { public void onItemClick(AdapterView<?> parent, View view, int position, long id) { onView(((BookViewHolder) view.getTag()).bookId); } } }