/****************************************************************************** * Copyright (C) 2012, 2013, 2014, 2015, 2016 * Younghyung Cho. <yhcting77@gmail.com> * All rights reserved. * * This file is part of NetMBuddy * * 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.netmbuddy; import android.content.Intent; import android.graphics.Bitmap; import android.os.Bundle; import android.support.annotation.NonNull; import android.view.ContextMenu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import static free.yhc.abaselib.util.UxUtil.showTextToast; import free.yhc.abaselib.AppEnv; import free.yhc.baselib.Logger; import free.yhc.baselib.async.Task; import free.yhc.baselib.async.TmTask; import free.yhc.abaselib.util.AUtil; import free.yhc.abaselib.util.ImgUtil; import free.yhc.abaselib.ux.DialogTask; import free.yhc.netmbuddy.core.UnexpectedExceptionHandler; import free.yhc.netmbuddy.core.YTDataAdapter; import free.yhc.netmbuddy.db.DB; import free.yhc.netmbuddy.core.YTPlayer; import free.yhc.netmbuddy.db.DMVideo; import free.yhc.netmbuddy.task.YTVideoListTask; import free.yhc.netmbuddy.utils.UxUtil; import free.yhc.netmbuddy.utils.Util; public abstract class YTVideoSearchActivity extends YTSearchActivity implements UnexpectedExceptionHandler.Evidence { private static final boolean DBG = Logger.DBG_DEFAULT; private static final Logger P = Logger.create(YTVideoSearchActivity.class, Logger.LOGLV_DEFAULT); private final DB mDb = DB.get(); private final YTPlayer mMp = YTPlayer.get(); private View.OnClickListener mToolBtnSearchAction; private DBCheckDupTask mDbCheckDupTask = null; private final DBCheckDupTask.EventListener<DBCheckDupTask, boolean[]> mDbCheckDupTaskListener = new DBCheckDupTask.EventListener<DBCheckDupTask, boolean[]>() { @Override public void onPostRun(@NonNull DBCheckDupTask task, boolean[] result, Exception ex) { Err err = Err.NO_ERR; if (null != ex) err = Err.DB_UNKNOWN; checkDupDone(task, task.getVideos(), result, err); } }; private YTVideoSearchAdapter.CheckStateListener mAdapterCheckListener = new YTVideoSearchAdapter.CheckStateListener() { @Override public void onStateChanged(int nrChecked, int pos, boolean checked) { if (0 == nrChecked) { setupToolBtn1(getToolButtonSearchIcon(), mToolBtnSearchAction); } else { setupToolBtn1(R.drawable.ic_add, new View.OnClickListener() { @Override public void onClick(View v) { addCheckedMusicsTo(); } }); } } }; private final OnPlayerUpdateDBListener mOnPlayerUpdateDbListener = new OnPlayerUpdateDBListener(); private class OnPlayerUpdateDBListener implements YTPlayer.OnDBUpdatedListener { @Override public void onDbUpdated(YTPlayer.DBUpdateType ty) { switch (ty) { case PLAYLIST: enableContentLoading(); YTVideoSearchAdapter adapter = getAdapter(); if (null != adapter) checkDupAsync(null, adapter.getItems()); } // others are ignored. } } private class DBCheckDupTask extends TmTask<boolean[]> { private final YTDataAdapter.Video[] mVids; private final Object mOpaque; DBCheckDupTask(@NonNull YTDataAdapter.Video[] vids, Object opaque) { mVids = vids; mOpaque = opaque; } public YTDataAdapter.Video[] getVideos() { return mVids; } public Object getOpaque() { return mOpaque; } @Override @NonNull public boolean[] doAsync() throws InterruptedException { // TODO : Should I check "entries[i].available" flag??? boolean[] r = new boolean[mVids.length]; publishProgressInit(r.length); publishProgress(0); for (int i = 0; i < r.length; i++) { r[i] = DB.get().containsVideo(mVids[i].id); if (isCancel()) throw new InterruptedException("Task is cancelled"); publishProgress(i + 1); } return r; } } /////////////////////////////////////////////////////////////////////////// // // // /////////////////////////////////////////////////////////////////////////// private YTVideoSearchAdapter getAdapter() { return (YTVideoSearchAdapter)mListv.getAdapter(); } private void onContextMenuAddTo(final int position) { UxUtil.OnPlaylistSelected action = new UxUtil.OnPlaylistSelected() { @Override public void onPlaylist(long plid, Object user) { int pos = (Integer)user; int volume = getAdapter().getItemVolume(pos); int msg = addToPlaylist(getAdapter(), plid, pos, volume); if (0 != msg) showTextToast(msg); } @Override public void onUserMenu(int pos, Object user) {} }; UxUtil.buildSelectPlaylistDialog(mDb, this, R.string.add_to, null, action, DB.INVALID_PLAYLIST_ID, position) .show(); } private void onContextMenuAppendToPlayQ(final int position) { YTPlayer.Video vid = getAdapter().getYTPlayerVideo(position); appendToPlayQ(new YTPlayer.Video[]{vid}); } private void onContextMenuPlayVideo(final int position) { UxUtil.playAsVideo(this, getAdapter().getItemVideoId(position)); } private void onContextMenuVideosOfSameChannel(final int position) { Intent i = new Intent(this, YTVideoSearchChannelActivity.class); i.putExtra(YTSearchActivity.KEY_TITLE, getAdapter().getItemChannelTitle(position)); i.putExtra(YTSearchActivity.KEY_TEXT, getAdapter().getItemChannelId(position)); startActivity(i); } private void onContextMenuSearchSimilarTitles(final int position) { UxUtil.showSimilarTitlesDialog(this, getAdapter().getItemTitle(position)); } private void checkDupAsync(Object tag, YTDataAdapter.Video[] vids) { if (null != mDbCheckDupTask) { mDbCheckDupTask.removeEventListener(mDbCheckDupTaskListener); mTm.cancelTask(mDbCheckDupTask, null); } mDbCheckDupTask = new DBCheckDupTask(vids, tag); mDbCheckDupTask.addEventListener(AppEnv.getUiHandlerAdapter(), mDbCheckDupTaskListener); if (!mTm.addTask( mDbCheckDupTask, mDbCheckDupTask, this, null)) { P.bug(false); // This is UNEXPECTED! } } private void checkDupDoneNewEntries(YTDataAdapter.Video[] vids, boolean[] results) { // helper's event receiver is changed to adapter in adapter's constructor. YTVideoSearchAdapter adapter = new YTVideoSearchAdapter(this, vids); adapter.setCheckStateListener(getAdapterCheckStateListener()); // First request is done! // Now we know total Results. // Let's build adapter and enable list. applyDupCheckResults(adapter, results); YTVideoSearchAdapter oldAdapter = getAdapter(); mListv.setAdapter(adapter); // Cleanup before as soon as possible to secure memories. if (null != oldAdapter) oldAdapter.cleanup(); } private void checkDupDone(DBCheckDupTask task, YTDataAdapter.Video[] vids, boolean[] results, Err err) { if (task != mDbCheckDupTask) return; //new task is already started. Ignore this result. if (Err.NO_ERR != err || results.length != vids.length) { enableContentText(R.string.err_db_unknown); return; } enableContentList(); if (null != getAdapter() && vids == getAdapter().getItems()) // Entry is same with current adapter. // That means 'dup. checking is done for exsiting entries" applyDupCheckResults(getAdapter(), results); else checkDupDoneNewEntries(vids, results); } private void applyDupCheckResults(YTVideoSearchAdapter adapter, boolean[] results) { for (int i = 0; i < results.length; i++) { if (results[i]) adapter.setToDup(i); else adapter.setToNew(i); } } private void addCheckedMusicsTo() { final int[] menuTextIds = new int[] { R.string.append_to_playq }; final String[] userMenu = new String[menuTextIds.length]; for (int i = 0; i < menuTextIds.length; i++) userMenu[i] = AUtil.getResString(menuTextIds[i]); UxUtil.OnPlaylistSelected action = new UxUtil.OnPlaylistSelected() { @Override public void onPlaylist(final long plid, Object user) { addCheckedMusicsToPlaylist(plid); } @Override public void onUserMenu(int pos, Object user) { P.bug(0 <= pos && pos < menuTextIds.length); switch (menuTextIds[pos]) { case R.string.append_to_playq: appendCheckMusicsToPlayQ(); break; default: P.bug(false); } } }; UxUtil.buildSelectPlaylistDialog(mDb, this, R.string.add_to, userMenu, action, DB.INVALID_PLAYLIST_ID, null) .show(); } private void addCheckedMusicsToPlaylist(final long plid) { // Scan to check all thumbnails are loaded. // And prepare data for background execution. final YTVideoSearchAdapter adpr = getAdapter(); final int[] checkedItems = adpr.getCheckedItem(); final int[] itemVolumes = new int[checkedItems.length]; for (int i = 0; i < checkedItems.length; i++) { int pos = checkedItems[i]; if (null == adpr.getItemThumbnail(pos)) { showTextToast(R.string.msg_no_all_thumbnail); return; } itemVolumes[i] = adpr.getItemVolume(pos); } Task<Void> t = new Task<Void>() { @Override protected Void doAsync() { int failedCnt = 0; mDb.beginTransaction(); try { for (int i = 0; i < checkedItems.length; i++) { int pos = checkedItems[i]; int r = addToPlaylist(getAdapter(), plid, pos, itemVolumes[i]); if (0 != r && R.string.msg_existing_muisc != r) failedCnt++; } mDb.setTransactionSuccessful(); } finally { mDb.endTransaction(); } final int failedCnt_ = failedCnt; AppEnv.getUiHandler().post(new Runnable() { @Override public void run() { adpr.cleanChecked(); if (failedCnt_ > 0) { CharSequence msg = getResources().getText(R.string.msg_fails_to_add); showTextToast(msg + " : " + failedCnt_); } } }); return null; } }; DialogTask.Builder<DialogTask.Builder> b = new DialogTask.Builder<>(this, t); b.setMessage(R.string.adding); if (!b.create().start()) P.bug(); } private void appendCheckMusicsToPlayQ() { // # of adapter items are at most Policy.YTSEARCH_MAX_RESULTS // So, just do it at main UI thread! YTVideoSearchAdapter adpr = getAdapter(); int[] checkedItems = adpr.getCheckItemSortedByTime(); YTPlayer.Video[] vids = new YTPlayer.Video[checkedItems.length]; int j = 0; for (int i : checkedItems) vids[j++] = adpr.getYTPlayerVideo(i); appendToPlayQ(vids); adpr.cleanChecked(); } /////////////////////////////////////////////////////////////////////////// // // // /////////////////////////////////////////////////////////////////////////// protected void onCreateInternal(String title, String text) { mToolBtnSearchAction = new View.OnClickListener() { @Override public void onClick(View v) { doNewSearch(); } }; setupBottomBar(getToolButtonSearchIcon(), mToolBtnSearchAction, 0, null); if (null != text) startNewSearch(title, text); else doNewSearch(); } /** * Override it to enabel tool button for search. */ protected int getToolButtonSearchIcon() { return 0; } public YTVideoSearchAdapter.CheckStateListener getAdapterCheckStateListener() { return mAdapterCheckListener; } public void appendToPlayQ(YTPlayer.Video[] vids) { mMp.appendToPlayQ(vids); } /** * @return * 0 for success, otherwise error message id. */ public int addToPlaylist(final YTVideoSearchAdapter adapter, final long plid, final int pos, final int volume) { // NOTE // This function is designed to be able to run in background. // But, getting volume is related with YTPlayer instance. // And lots of functions of YTPlayer instance, requires running on UI Context // to avoid synchronization issue. // So, volume should be gotten out of this function. P.bug(plid >= 0); Bitmap bm = adapter.getItemThumbnail(pos); if (null == bm) { return R.string.msg_no_thumbnail; } final YTDataAdapter.Video ytv = (YTDataAdapter.Video)adapter.getItem(pos); DMVideo v = new DMVideo(); v.setYtData(ytv); v.setThumbnail(ImgUtil.compressToJpeg(bm)); v.setPreferenceData(volume, ""); DB.Err err = mDb.insertVideoToPlaylist(plid, v); if (DB.Err.NO_ERR != err) { if (DB.Err.DUPLICATED == err) return R.string.msg_existing_muisc; else return Err.map(err).getMessage(); } this.runOnUiThread(new Runnable() { @Override public void run() { adapter.setToDup(pos); } }); return 0; } /////////////////////////////////////////////////////////////////////////// // // // /////////////////////////////////////////////////////////////////////////// @Override protected void onListItemClick(View view, int position, long itemId) { if (!Util.isNetworkAvailable()) { showTextToast(Err.IO_NET.getMessage()); return; } YTPlayer.Video v = getAdapter().getYTPlayerVideo(position); mMp.startVideos(new YTPlayer.Video[] { v }); } @Override protected void onSearchResponse(@NonNull YTVideoListTask ytvl, @NonNull YTDataAdapter.VideoListReq req, @NonNull YTDataAdapter.VideoListResp resp, @NonNull Err err) { checkDupAsync(req, resp.vids); } /////////////////////////////////////////////////////////////////////////// // // // /////////////////////////////////////////////////////////////////////////// @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.ytvideosearch_context, menu); AdapterView.AdapterContextMenuInfo mInfo = (AdapterView.AdapterContextMenuInfo)menuInfo; // menu 'videos of same channel' is useless if we are already in the same-type-search boolean visible = Util.isValidValue(getAdapter().getItemChannelTitle(mInfo.position)) && YTDataAdapter.ReqType.VID_CHANNEL != getSearchType(); menu.findItem(R.id.videos_of_same_channel).setVisible(visible); } @Override public boolean onContextItemSelected(MenuItem mItem) { AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)mItem.getMenuInfo(); switch (mItem.getItemId()) { case R.id.add_to: onContextMenuAddTo(info.position); return true; case R.id.append_to_playq: onContextMenuAppendToPlayQ(info.position); return true; case R.id.play_video: onContextMenuPlayVideo(info.position); return true; case R.id.videos_of_same_channel: onContextMenuVideosOfSameChannel(info.position); return true; case R.id.search_similar_titles: onContextMenuSearchSimilarTitles(info.position); return true; } return false; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); UnexpectedExceptionHandler.get().registerModule(this); } @Override public void onResume() { super.onResume(); mMp.addOnDbUpdatedListener(mOnPlayerUpdateDbListener); if (mDb.isRegisteredToVideoTableWatcher(this)) { if (mDb.isVideoTableUpdated(this) && null != getAdapter()) { enableContentLoading(); checkDupAsync(null, getAdapter().getItems()); } mDb.unregisterToVideoTableWatcher(this); } } @Override public void onPause() { mMp.removeOnDbUpdatedListener(mOnPlayerUpdateDbListener); mDb.registerToVideoTableWatcher(this); super.onPause(); } @Override protected void onDestroy() { mDb.unregisterToVideoTableWatcher(this); UnexpectedExceptionHandler.get().unregisterModule(this); super.onDestroy(); } }