From bf48d3ccd0313d17961ffaa239d8dfa189e480fe Mon Sep 17 00:00:00 2001 From: Matthew Adler Date: Tue, 30 Dec 2014 02:33:03 -0800 Subject: [PATCH] Addresses issue #20, implements asset image view updated at measured latency. Support for pause, but not implemented currently --- .../roundware/activity/ListenActivity.java | 196 ++++++++++++++++-- .../org/famsf/roundware/utils/AssetData.java | 27 +++ .../roundware/utils/AssetImageManager.java | 12 +- 3 files changed, 205 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/org/famsf/roundware/utils/AssetData.java diff --git a/app/src/main/java/org/famsf/roundware/activity/ListenActivity.java b/app/src/main/java/org/famsf/roundware/activity/ListenActivity.java index d0aa153..7fc74d2 100644 --- a/app/src/main/java/org/famsf/roundware/activity/ListenActivity.java +++ b/app/src/main/java/org/famsf/roundware/activity/ListenActivity.java @@ -50,13 +50,22 @@ import org.famsf.roundware.R; import org.famsf.roundware.Settings; +import org.famsf.roundware.utils.AssetData; import org.famsf.roundware.utils.AssetImageManager; import org.famsf.roundware.utils.LocationBg; import org.famsf.roundware.utils.Utils; import java.io.IOException; +import java.util.ArrayList; import java.util.List; import java.util.Random; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; public class ListenActivity extends Activity { private final static String LOGTAG = "Listen"; @@ -69,6 +78,8 @@ public class ListenActivity extends Activity { private final static String AS_VOTE_TYPE_FLAG = "flag"; private final static String AS_VOTE_TYPE_LIKE = "like"; + private final static int ASSET_IMAGE_LINGER_MS = 5200; + // fields private ProgressDialog mProgressDialog; private ViewFlipper mViewFlipper; @@ -84,8 +95,12 @@ public class ListenActivity extends Activity { private View mAssetImageLayout; private ImageView mAssetImageView; private TextView mAssetTextView; - private String mAssetImageUrl; - private String mAssetImageDescription; + private AssetData mPendingAsset = new AssetData(null,null); + private AssetData mCurrentAsset = mPendingAsset; + + + private PausableScheduledThreadPoolExecutor mEventPool; + private final Object mAssetImageLock = new Object(); // private ToggleButton mLikeButton; @@ -97,6 +112,8 @@ public class ListenActivity extends Activity { private String mContentFileDir; private int mCurrentAssetId; private int mPreviousAssetId; + private long mStartTime = 0; + private long mMetaPlayLatency = 0; private AssetImageManager mAssetImageManager = null; LocationListener mLocationListener = new LocationListener() { @@ -167,6 +184,7 @@ public void onServiceDisconnected(ComponentName name) { }; + /** * Handles events received from the RWService Android Service that we * connect to. Since most operations of the service involve making calls @@ -186,15 +204,12 @@ public void onReceive(Context context, Intent intent) { if (mProgressDialog != null) { mProgressDialog.dismiss(); } + if(mMetaPlayLatency == 0){ + mMetaPlayLatency = System.currentTimeMillis() - mStartTime; + Log.v(LOGTAG, "Metadata latency estimated as " + mMetaPlayLatency); + } } else if (RW.STREAM_METADATA_UPDATED.equals(intent.getAction())) { if (D) { Log.d(LOGTAG, "RW_STREAM_METADATA_UPDATED"); } - // new asset started playing - update image display - // remove progress dialog when needed - if (mProgressDialog != null) { - mProgressDialog.dismiss(); - mProgressDialog = null; - } - handleAssetChange( (Uri)intent.getParcelableExtra(RW.EXTRA_STREAM_METADATA_URI) ); } else if (RW.USER_MESSAGE.equals(intent.getAction())) { @@ -222,6 +237,14 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_listen); initUIWidgets(); + mEventPool = new PausableScheduledThreadPoolExecutor(2); + mEventPool.setRejectedExecutionHandler( new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + Log.w(LOGTAG, "Event was rejected!"); + } + }); + mEventPool.setKeepAliveTime(ASSET_IMAGE_LINGER_MS, TimeUnit.MILLISECONDS); // connect to service started by other activity try { @@ -288,6 +311,10 @@ protected void onResume() { @Override protected void onDestroy() { + if(mEventPool != null){ + mEventPool.purge(); + mEventPool.shutdownNow(); + } if (rwConnection != null) { unbindService(rwConnection); } @@ -462,7 +489,9 @@ private void startPlayback() { showProgress(getString(R.string.starting_playback_title), getString(R.string.starting_playback_message), true, true); mCurrentAssetId = -1; mPreviousAssetId = -1; - setAssetImage(null, null); + AssetData assetData = new AssetData(null, null); + setPendingAsset(assetData); + setCurrentAsset(assetData); mRwBinder.playbackStart(mTagsList); } mRwBinder.playbackFadeIn(mVolumeLevel); @@ -478,7 +507,9 @@ private void stopPlayback() { mRwBinder.playbackFadeOut(); mCurrentAssetId = -1; mPreviousAssetId = -1; - setAssetImage(null, null); + AssetData assetData = new AssetData(null, null); + setPendingAsset(assetData); + setCurrentAsset(assetData); updateUIState(); } @@ -490,6 +521,9 @@ private void handleAssetChange(Uri uri) { Log.d(LOGTAG, "handleAssetChange param null!"); return; } + if(mStartTime == 0){ + mStartTime = System.currentTimeMillis(); + } String assetValue = uri.getQueryParameter(RW.METADATA_URI_NAME_ASSET_ID); int assetId = -1; if(!TextUtils.isEmpty(assetValue)){ @@ -505,6 +539,13 @@ private void handleAssetChange(Uri uri) { sendVotingState(mPreviousAssetId); List tags = RWUriHelper.getQueryParameterValues(uri, RW.METADATA_URI_NAME_TAGS); + if(tags.isEmpty()){ + String remaining = RWUriHelper.getQueryParameter(uri, RW.METADATA_URI_NAME_REMAINING); + if(remaining != null && !remaining.equals("0") ){ + // this metadata message is verbose, remaining asset probably still has same image + return; + } + } // update display String url = null; @@ -529,8 +570,9 @@ private void handleAssetChange(Uri uri) { } } - setAssetImage(url, description); - updateAssetImageUi(); + AssetData assetData = new AssetData(url, description); + setPendingAsset(assetData); + mEventPool.schedule(new AssetEvent(assetData), mMetaPlayLatency, TimeUnit.MILLISECONDS); } @@ -547,22 +589,37 @@ private void sendVotingState(int assetId) { */ } - private void setAssetImage(String url, String description){ + + private void setPendingAsset(AssetData asset) { + synchronized (mAssetImageLock) { + mPendingAsset = asset; + } + } + private void setCurrentAsset(AssetData asset){ synchronized (mAssetImageLock){ - mAssetImageUrl = url; - mAssetImageDescription = description; + mCurrentAsset = asset; } } + private void updateAssetImageUi(){ + if(mAssetImageView == null || mAssetTextView == null){ + //panic + Log.w(LOGTAG, "An Asset Image View is null!"); + return; + } + if(!mCurrentAsset.equals(mPendingAsset) && mCurrentAsset.url == null){ + //pending appears valid, do not hide yet + return; + } synchronized (mAssetImageLock) { - boolean hasUrl = !TextUtils.isEmpty(mAssetImageUrl); + boolean hasUrl = !TextUtils.isEmpty(mCurrentAsset.url); if(hasUrl){ //load Picasso picasso = Picasso.with(this); // set below true, to view image source debugging picasso.setIndicatorsEnabled(false); - picasso.load(mAssetImageUrl) + picasso.load(mCurrentAsset.url) .into(mAssetImageView, new Callback() { @Override public void onSuccess() { } @@ -575,7 +632,7 @@ public void onError() { }); } //TODO fade? - mAssetTextView.setText(mAssetImageDescription); + mAssetTextView.setText(mCurrentAsset.description); mAssetImageLayout.setVisibility(hasUrl ? View.VISIBLE : View.INVISIBLE); } } @@ -740,5 +797,106 @@ protected void onPostExecute(String result) { } } + private class AssetEvent implements Runnable{ + + private final AssetData assetData; + public AssetEvent(AssetData assetData){ + this.assetData = assetData; + } + + @Override + public void run() { + synchronized (mAssetImageLock){ + if(mPendingAsset.equals(assetData)){ + AssetData previousAsset = mCurrentAsset; + setCurrentAsset(assetData); + if(TextUtils.isEmpty(assetData.url) && !TextUtils.isEmpty(previousAsset.url)){ + mEventPool.schedule(new AssetEvent(assetData), ASSET_IMAGE_LINGER_MS, + TimeUnit.MILLISECONDS); + }else{ + Log.v(LOGTAG, "AssetEvent now, now drawing"); + runOnUiThread(new Runnable() { + @Override + public void run() { + updateAssetImageUi(); + } + }); + } + } + } + } + } + + + + + /** + * Adds pausing, adapted from Android Developers Documentation + * http://developer.android.com/reference/java/util/concurrent/ThreadPoolExecutor.html + */ + private class PausableScheduledThreadPoolExecutor extends ScheduledThreadPoolExecutor{ + + public PausableScheduledThreadPoolExecutor(int corePoolSize) { + super(corePoolSize); + pauseQueue = new ArrayList(); + } + private boolean isPaused; + private ReentrantLock pauseLock = new ReentrantLock(); + + private class PausedRunnable{ + public final Runnable runnable; + public final long delay; + public PausedRunnable(Runnable runnable, long delay){ + this.runnable = runnable; + this.delay = delay; + } + } + + private ArrayList pauseQueue; + + private void add(Runnable r){ + ScheduledFuture sf = (ScheduledFuture)r; + pauseQueue.add( new PausedRunnable(r, sf.getDelay(TimeUnit.MILLISECONDS)) ); + sf.cancel(false); + } + + protected void beforeExecute(Thread t, Runnable r) { + super.beforeExecute(t, r); + pauseLock.lock(); + + if (isPaused) { + add(r); + } + + pauseLock.unlock(); + + } + + public void pause() { + pauseLock.lock(); + try { + isPaused = true; + BlockingQueue queue = getQueue(); + for(Runnable r : queue){ + add(r); + } + } finally { + pauseLock.unlock(); + } + } + + public void resume() { + pauseLock.lock(); + try { + isPaused = false; + for(PausedRunnable pausedRunnable : pauseQueue){ + this.schedule(pausedRunnable.runnable, pausedRunnable.delay, TimeUnit.MILLISECONDS); + } + } finally { + pauseLock.unlock(); + } + } + } + } diff --git a/app/src/main/java/org/famsf/roundware/utils/AssetData.java b/app/src/main/java/org/famsf/roundware/utils/AssetData.java new file mode 100644 index 0000000..8c96500 --- /dev/null +++ b/app/src/main/java/org/famsf/roundware/utils/AssetData.java @@ -0,0 +1,27 @@ +package org.famsf.roundware.utils; + +/** + * A simple class with two strings + */ +public class AssetData { + public final String url; + public final String description; + + public AssetData(String url, String description){ + this.url = url; + this.description = description; + } + + @Override + public boolean equals(Object o) { + if(o instanceof AssetData){ + AssetData other = (AssetData)o; + boolean urlTest = other.url == null ? this.url == null : other.url.equals(this.url); + boolean descriptionTest = other.description == null ? this.description == null : + other.description.equals(this.description); + return urlTest && descriptionTest; + } + return false; + } +} + diff --git a/app/src/main/java/org/famsf/roundware/utils/AssetImageManager.java b/app/src/main/java/org/famsf/roundware/utils/AssetImageManager.java index b373ffe..d55e798 100644 --- a/app/src/main/java/org/famsf/roundware/utils/AssetImageManager.java +++ b/app/src/main/java/org/famsf/roundware/utils/AssetImageManager.java @@ -25,16 +25,6 @@ public class AssetImageManager { public final static String PREFS_IMAGE_PREFIX = "asset_img_"; public final static String PREFS_DESCRIPTION_PREFIX = "asset_txt_"; - private static class AssetData { - public final String urlSuffix; - public final String description; - - public AssetData(String urlSuffix, String description){ - this.urlSuffix = urlSuffix; - this.description = description; - } - } - private SparseArray map = new SparseArray(INITIAL_SIZE); private final String hostUrl; @@ -88,7 +78,7 @@ private String getImageUrlSuffix(int tagId){ String out = null; AssetData data = map.get(tagId); if(data != null){ - out = data.urlSuffix; + out = data.url; } if(TextUtils.isEmpty(out)){ // fall back on SharedPreferences cache