/* * Copyright 2014 Bevbot LLC <info@bevbot.com> * * This file is part of the Kegtab package from the Kegbot project. For * more information on Kegtab or Kegbot, see <http://kegbot.org/>. * * Kegtab is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free * Software Foundation, version 2. * * Kegtab is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with Kegtab. If not, see <http://www.gnu.org/licenses/>. */ package org.kegbot.core; import android.content.Context; import android.media.AudioManager; import android.media.MediaPlayer; import android.util.Log; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Queues; import com.google.common.collect.Sets; import com.squareup.otto.Bus; import com.squareup.otto.Subscribe; import org.kegbot.app.event.Event; import org.kegbot.app.event.FlowUpdateEvent; import org.kegbot.app.event.SoundEventListUpdateEvent; import org.kegbot.app.util.Downloader; import org.kegbot.app.util.IndentingPrintWriter; import org.kegbot.app.util.Units; import org.kegbot.proto.Models.SoundEvent; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; /** * Plays sounds at specific events. * * @author mike wakerly (opensource@hoho.com) */ public class SoundManager extends BackgroundManager { private static final String TAG = SoundManager.class.getSimpleName(); private static final boolean DEBUG = false; private static final String EVENT_FLOW_THRESHOLD_OUNCES = "flow.threshold.ounces"; /** Queue of Events, shipped from main thread to our background thread. */ private final LinkedBlockingQueue<Event> mCommandQueue = Queues.newLinkedBlockingQueue(); /** All sound events. */ private final Set<SoundEvent> mSoundEvents = Sets.newLinkedHashSet(); /** Map of flows to last observed volume. */ private final Map<Flow, Double> mFlowsByLastVolume = Maps.newLinkedHashMap(); /** Map of URLs to locally-cached files. */ private final Map<String, File> mFiles = Maps.newLinkedHashMap(); /** Executor for playing sounds. */ private final ExecutorService mExecutor = Executors.newFixedThreadPool(3); /** Map of volume (mL) to sound event. */ private final Map<Double, SoundEvent> mVolumeThresholds = Maps.newLinkedHashMap(); private Context mContext; private MediaPlayer mMediaPlayer; private boolean mQuit; public SoundManager(Bus bus, Context context) { super(bus); mContext = context; } @Override public synchronized void start() { mMediaPlayer = new MediaPlayer(); mQuit = false; getBus().register(this); super.start(); } @Override public synchronized void stop() { mQuit = true; mCommandQueue.clear(); mMediaPlayer.release(); getBus().unregister(this); super.stop(); } @Override protected void dump(IndentingPrintWriter writer) { writer.printPair("numFlows", Integer.valueOf(mFlowsByLastVolume.size()).toString()).println(); if (!mFlowsByLastVolume.isEmpty()) { writer.println("Flows:"); writer.increaseIndent(); for (Map.Entry<Flow, Double> entry : mFlowsByLastVolume.entrySet()) { writer.printPair("flowId", Integer.valueOf(entry.getKey().getFlowId()).toString()) .println(); writer.printPair("lastVolumeMl", entry.getValue().toString()) .println(); writer.println(); } } } @Override protected void runInBackground() { while (true) { synchronized (this) { if (mQuit) { break; } } Event event; try { event = mCommandQueue.poll(200, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); continue; } if (event != null) { if (DEBUG) { Log.d(TAG, "Handling event: " + event + " mSoundEvents.size=" + mSoundEvents.size()); } if (event instanceof FlowUpdateEvent) { processFlowUpdate(((FlowUpdateEvent) event).getFlow()); } else if (event instanceof SoundEventListUpdateEvent) { processSoundEventListUpdateEvent((SoundEventListUpdateEvent) event); } else { if (DEBUG) Log.w(TAG, "Unrecognized event type."); } } } } /** Background callback, handles a flow update. */ private void processFlowUpdate(Flow flow) { if (flow.isFinished()) { mFlowsByLastVolume.remove(flow); return; } final Double currentVolume = Double.valueOf(flow.getVolumeMl()); Double lastVolume = mFlowsByLastVolume.get(flow); if (lastVolume == null) { lastVolume = Double.valueOf(0); } mFlowsByLastVolume.put(flow, currentVolume); if (currentVolume == lastVolume) { return; } // Find the next volume threshold, based on lastVolume. List<Double> thresholds = Lists.newArrayList(mVolumeThresholds.keySet()); Collections.sort(thresholds); Double threshold = null; for (Double v : thresholds) { if (v.doubleValue() > lastVolume.doubleValue()) { threshold = v; break; } } if (threshold == null) { if (DEBUG) Log.d(TAG, "No threshold."); return; } if (currentVolume.doubleValue() >= threshold.doubleValue()) { Log.d(TAG, "Tripped threshold: " + threshold); SoundEvent e = mVolumeThresholds.get(threshold); if (e == null) { Log.e(TAG, "No event."); return; } playSound(e); } } /** Background callback, handles an updated sound event list. */ private void processSoundEventListUpdateEvent(SoundEventListUpdateEvent event) { final Set<SoundEvent> newEvents = Sets.newHashSet(event.getEvents()); Log.d(TAG, "Updated sound events: " + Joiner.on(", ").join(newEvents)); if (!newEvents.equals(mSoundEvents)) { mSoundEvents.clear(); Log.d(TAG, "New/updated sound events: "); for (SoundEvent e : newEvents) { Log.d(TAG, "Event: " + e); downloadEvent(e); mSoundEvents.add(e); } } recomputeVolumeThresholds(); } /** Downloads a single file. */ private void downloadEvent(SoundEvent e) { final String url = e.getSoundUrl(); if (mFiles.containsKey(url)) { final File output = mFiles.get(url); if (output.exists()) { return; } else { mFiles.remove(url); } } String[] parts = url.split("/"); final String filename = parts[parts.length - 1]; final File output = new File(mContext.getCacheDir(), filename); if (output.exists()) { Log.d(TAG, "File exists: " + url + " file=" + output); mFiles.put(url, output); return; } final Runnable job = new Runnable() { @Override public void run() { Log.d(TAG, "Downloading: " + url); try { Downloader.downloadRaw(url, output); } catch (IOException exc) { Log.w(TAG, "Download failed: " + exc.toString(), exc); return; } Log.d(TAG, "Sucesss: " + url + " file=" + output); mFiles.put(url, output); } }; mExecutor.submit(job); } /** Plays a single, previously-downloaded sound. */ private void playSound(SoundEvent event) { final String soundUrl = event.getSoundUrl(); Log.d(TAG, "Playing sound: " + soundUrl); final File soundFile = mFiles.get(soundUrl); if (soundFile == null) { Log.w(TAG, String.format("Can't find cached file for url: %s", soundUrl)); return; } mMediaPlayer.reset(); mMediaPlayer.setAudioStreamType(AudioManager.STREAM_NOTIFICATION); FileInputStream fis; try { fis = new FileInputStream(soundFile); try { mMediaPlayer.setDataSource(fis.getFD()); mMediaPlayer.prepare(); mMediaPlayer.start(); } catch (IllegalArgumentException e) { Log.w(TAG, "Error", e); } catch (IllegalStateException e) { Log.w(TAG, "Error", e); } catch (IOException e) { Log.w(TAG, "Error", e); } finally { try { fis.close(); } catch (IOException e) { // Close quietly. } } } catch (FileNotFoundException e) { Log.w(TAG, "Error loading file: " + e.toString(), e); return; } } @Subscribe public void onFlowUpdateEvent(FlowUpdateEvent event) { mCommandQueue.add(event); } @Subscribe public void onSoundEventListUpdateEvent(SoundEventListUpdateEvent event) { mCommandQueue.add(event); } /** Recomputes {@link #mVolumeThresholds} based on {@link #mSoundEvents}. */ private void recomputeVolumeThresholds() { final Map<Double, SoundEvent> result = Maps.newLinkedHashMap(); for (final SoundEvent event : mSoundEvents) { if (!EVENT_FLOW_THRESHOLD_OUNCES.equals(event.getEventName())) { continue; } try { final Double ounces = Double.valueOf(event.getEventPredicate()); final Double ml = Double.valueOf(Units.volumeOuncesToMl(ounces.doubleValue())); result.put(ml, event); } catch (NumberFormatException e) { // Ignore } } if (!result.equals(mVolumeThresholds)) { mVolumeThresholds.clear(); mVolumeThresholds.putAll(result); Log.d(TAG, "Updated volume thresholds: " + Joiner.on(", ").join(result.keySet())); } } }