]> hydra-www.ietfng.org Git - acmetensortoys-watchviz/commitdiff
More refactoring
authorNathaniel Wesley Filardo <nwf@cs.jhu.edu>
Fri, 1 Jul 2016 22:41:29 +0000 (18:41 -0400)
committerNathaniel Wesley Filardo <nwf@cs.jhu.edu>
Fri, 1 Jul 2016 22:56:31 +0000 (18:56 -0400)
Plumb a GridViewPager into being.  Lots of wrestling with drawing gunk.

app/src/main/java/com/acmetensortoys/watchviz/MainActivity.java
app/src/main/java/com/acmetensortoys/watchviz/MainGridViewAdapter.java [new file with mode: 0644]
app/src/main/res/layout/activity_main.xml
vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/AudioCanvas.java
vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/AudioProvider.java
vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/Rendering.java
vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/RenderingSelector.java [deleted file]

index 914c6b4760217dbb539064efc84b226fdc842ca2..dd7cc5ab84f0f7997d782fb3b0a7b19906b03f65 100644 (file)
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
+/*
+  This is the glue code that provides the Activity that Android interacts with.
+  Most of the actual UI work of interest is in MainGridViewAdapter.
+ */
+
 package com.acmetensortoys.watchviz;
 
 import android.Manifest;
 import android.content.pm.PackageManager;
 import android.os.Bundle;
+import android.os.Handler;
 import android.support.annotation.NonNull;
 import android.support.wearable.activity.WearableActivity;
-import android.support.wearable.view.BoxInsetLayout;
+import android.support.wearable.view.GridViewPager;
 import android.util.Log;
-import android.view.SurfaceView;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
 import android.widget.TextView;
 
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.Locale;
 
-import com.acmetensortoys.watchviz.vizlib.AudioCanvas;
 import com.acmetensortoys.watchviz.vizlib.AudioProvider;
-import com.acmetensortoys.watchviz.vizlib.RenderingSelector;
 
 public class MainActivity extends WearableActivity
 {
     private static final SimpleDateFormat AMBIENT_DATE_FORMAT =
             new SimpleDateFormat("HH:mm", Locale.US);
+    private static final int TIME_UPDATE_NOT_AMBIENT_PERIOD = 2000;
 
-    private BoxInsetLayout mOuterContainer;
-    private LinearLayout mInnerContainer;
+    private Handler mHandler;
+    private RelativeLayout mTextContainer;
     private TextView mTextView, mClockView, mDebugView;
-    private SurfaceView mACSurfaceView;
-    private AudioCanvas mAudioCanvas;
     private AudioProvider mAudioProvider = new AudioProvider();
 
-    private void createSurface() {
-        Log.d("createSurface", "top");
-        mACSurfaceView = new SurfaceView(this);
-        mAudioCanvas = new AudioCanvas(mACSurfaceView, mAudioProvider);
+    private GridViewPager mGridView;
 
-        final RenderingSelector rs = new RenderingSelector(mDebugView, mAudioCanvas);
-
-        mTextView.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                rs.prevCyclerCB();
-            }
-        });
-        mACSurfaceView.setOnClickListener(new View.OnClickListener(){
-            @Override
-            public void onClick(View v) { mAudioCanvas.onClick(); }
-        });
-        mACSurfaceView.setOnLongClickListener(new View.OnLongClickListener() {
-            @Override
-            public boolean onLongClick(View v) {
-                rs.nextCyclerCB();
-                return true;
-            }
-        });
-        mInnerContainer.addView(mACSurfaceView, -1,
-                new ViewGroup.LayoutParams(
-                        ViewGroup.LayoutParams.MATCH_PARENT,
-                        ViewGroup.LayoutParams.MATCH_PARENT));
+    private void createSurfaces() {
+        Log.d("createSurfaces", "top");
+        MainGridViewAdapter mgva = new MainGridViewAdapter(this, mAudioProvider, mDebugView);
+        mGridView.setOnPageChangeListener(mgva);
+        mGridView.setAdapter(mgva);
+        // Fake the selection callback.  Why doesn't this happen automatically? :(
+        mgva.onPageSelected(mGridView.getCurrentItem().x, mGridView.getCurrentItem().y);
     }
 
-    private void removeSurface() {
-        Log.d("removeSurface", "removing view");
-        mInnerContainer.removeView(mACSurfaceView);
-        Log.d("removeSurface", "done");
+    private void removeSurfaces() {
+        Log.d("removeSurfaces", "removing grid view");
+        mGridView.setAdapter(null);
+        Log.d("removeSurfaces", "done");
     }
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+
+        // grab an interface to our local looper
+        mHandler = new Handler();
+
         setContentView(R.layout.activity_main);
         setAmbientEnabled();
 
-        mOuterContainer = (BoxInsetLayout) findViewById(R.id.top_container);
-        mInnerContainer = (LinearLayout) findViewById(R.id.linear_container);
+        mTextContainer = (RelativeLayout) findViewById(R.id.text_container);
         mTextView = (TextView) findViewById(R.id.text);
         mDebugView = (TextView) findViewById(R.id.dbg);
         mClockView = (TextView) findViewById(R.id.clock);
+        mGridView = (GridViewPager) findViewById(R.id.grid);
+        // mGridView.setBackgroundColor(Color.WHITE);
+        // mGridView.setDrawingCacheBackgroundColor(Color.WHITE);
+        // mGridView.setOverScrollMode(View.OVER_SCROLL_NEVER);
 
         updateDisplay();
     }
@@ -112,7 +100,7 @@ public class MainActivity extends WearableActivity
                 != PackageManager.PERMISSION_GRANTED) {
             requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, 1);
         } else {
-            createSurface();
+            createSurfaces();
         }
 
         Log.d("onStart", "BLE: " + getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE));
@@ -129,7 +117,7 @@ public class MainActivity extends WearableActivity
     public void onRequestPermissionsResult(int rq, @NonNull String[] ps, @NonNull int[] rs) {
         if(rq == 1) {
             if (rs[0] == PackageManager.PERMISSION_GRANTED) {
-                createSurface();
+                createSurfaces();
             } else {
                 mDebugView.setText("No audio");
             }
@@ -139,39 +127,58 @@ public class MainActivity extends WearableActivity
     @Override
     public void onEnterAmbient(Bundle ambientDetails) {
         super.onEnterAmbient(ambientDetails);
+        mHandler.removeCallbacks(updateTimeNotAmbient);
         updateDisplay();
     }
 
-    @Override
-    public void onUpdateAmbient() {
-        super.onUpdateAmbient();
-        mClockView.setText(AMBIENT_DATE_FORMAT.format(new Date()));
-    }
-
     @Override
     public void onExitAmbient() {
-        updateDisplay();
         super.onExitAmbient();
+        updateDisplay();
+        mHandler.postDelayed(updateTimeNotAmbient, TIME_UPDATE_NOT_AMBIENT_PERIOD);
     }
 
     private void updateDisplay() {
         final int black = getResources().getColor(android.R.color.black,null);
-        mClockView.setText(AMBIENT_DATE_FORMAT.format(new Date()));
+        final int white = getResources().getColor(android.R.color.white,null);
+        updateTime();
 
         if (isAmbient()) {
-            final int white = getResources().getColor(android.R.color.white,null);
-            mOuterContainer.setBackgroundColor(black);
+            mTextContainer.setBackgroundColor(black);
             mTextView.setTextColor(white);
             mClockView.setTextColor(white);
             mDebugView.setTextColor(white);
         } else {
-            mOuterContainer.setBackground(null);
+            mTextContainer.setBackgroundColor(white);
             mTextView.setTextColor(black);
             mClockView.setTextColor(black);
             mDebugView.setTextColor(black);
         }
     }
 
+    @Override
+    public void onUpdateAmbient() {
+        super.onUpdateAmbient();
+        updateTime();
+    }
+
+    private Runnable updateTimeNotAmbient = new Runnable() {
+        @Override
+        public void run() {
+            updateTime();
+            mHandler.postDelayed(this, TIME_UPDATE_NOT_AMBIENT_PERIOD);
+        }
+    };
+
+    private void updateTime() {
+        mClockView.post(new Runnable() {
+            @Override
+            public void run() {
+                mClockView.setText(AMBIENT_DATE_FORMAT.format(new Date()));
+            }
+        });
+    }
+
     @Override
     public void onPause() {
         Log.d("onPause", "top");
@@ -181,7 +188,7 @@ public class MainActivity extends WearableActivity
     @Override
     public void onStop() {
         Log.d("onStop", "top");
-        removeSurface();
+        removeSurfaces();
         super.onStop();
     }
 
diff --git a/app/src/main/java/com/acmetensortoys/watchviz/MainGridViewAdapter.java b/app/src/main/java/com/acmetensortoys/watchviz/MainGridViewAdapter.java
new file mode 100644 (file)
index 0000000..eed0c38
--- /dev/null
@@ -0,0 +1,166 @@
+/*
+    AcmeTensorToys WatchViz: a sound visualization application for Android Wear
+    Copyright (C) 2016 Nathaniel Wesley Filardo
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program 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 Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/* This file is responsible for navigation of the UI, through different renderers
+   and (eventually), their settings, as well as global preferences and all that.
+ */
+
+
+package com.acmetensortoys.watchviz;
+
+import android.content.Context;
+import android.support.wearable.view.GridPagerAdapter;
+import android.support.wearable.view.GridViewPager;
+import android.util.Log;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.acmetensortoys.watchviz.vizlib.AudioCanvas;
+import com.acmetensortoys.watchviz.vizlib.AudioProvider;
+import com.acmetensortoys.watchviz.vizlib.Rendering;
+import com.acmetensortoys.watchviz.vizlib.rendering.Grid;
+import com.acmetensortoys.watchviz.vizlib.rendering.WholeMax;
+
+public class MainGridViewAdapter
+        extends GridPagerAdapter
+        implements GridViewPager.OnPageChangeListener {
+
+    /* All returned Objects are actually Pages so we know what to do with them later */
+    private abstract static class InstantiatedPage {
+        abstract public boolean ownsview(View v);
+        abstract public void deinstantiate(MainGridViewAdapter m, ViewGroup g);
+    };
+    private static class ViewInstantiatedPage extends InstantiatedPage {
+        public View v;
+        public ViewInstantiatedPage(View v) { this.v = v; }
+        @Override
+        public boolean ownsview(View v2) { return v.equals(v2); }
+        @Override
+        public void deinstantiate(MainGridViewAdapter m, ViewGroup g) { g.removeView(v); }
+    }
+    private abstract static class Page {
+        public String title;
+        abstract public InstantiatedPage instantiate(MainGridViewAdapter m, ViewGroup g, int r, int c);
+    }
+    private static class RendererPage extends Page {
+        private Class<? extends Rendering> re;
+        public RendererPage(Class<? extends Rendering> r) {
+            this.re = r;
+            this.title = r.getSimpleName();
+        }
+        public InstantiatedPage instantiate(MainGridViewAdapter mgva, ViewGroup g, int r, int c) {
+            SurfaceView sv = new SurfaceView(mgva.mCtx);
+            AudioCanvas ac = new AudioCanvas(r+"x"+c, sv, mgva.mAp);
+            try {
+                ac.setRendering(re.getConstructor().newInstance());
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+            g.addView(sv, -1,
+                    new ViewGroup.LayoutParams(
+                            ViewGroup.LayoutParams.MATCH_PARENT,
+                            ViewGroup.LayoutParams.MATCH_PARENT));
+            return new ViewInstantiatedPage(sv);
+        }
+    }
+
+    private Page gridPage = new RendererPage(Grid.class);
+    private Page wholePage = new RendererPage(WholeMax.class);
+    private final Page[][] pages = {
+            {gridPage, gridPage},
+            {wholePage }
+    };
+
+    private final Context mCtx;
+    private final AudioProvider mAp;
+    private final TextView mDv;
+
+    public MainGridViewAdapter(Context c, AudioProvider ap, TextView dv) {
+        mCtx = c;
+        mAp = ap;
+        mDv = dv;
+    }
+
+    @Override
+    public int getRowCount() {
+        return pages.length;
+    }
+
+    @Override
+    public int getCurrentColumnForRow(int row, int currentColumn) {
+        return 0;
+    }
+
+    @Override
+    public int getColumnCount(int i) {
+        return pages[i].length;
+    }
+
+    @Override
+    public Object instantiateItem(ViewGroup container, int row, int col) {
+        Log.d("MGVA", "instantiate:" + row + "x" + col);
+        return pages[row][col].instantiate(this, container, row, col);
+    }
+
+    @Override
+    public boolean isViewFromObject(View view, Object o) {
+        return ((InstantiatedPage)o).ownsview(view);
+    }
+
+    @Override
+    public void destroyItem(ViewGroup viewGroup, int row, int col, Object o) {
+        Log.d("MGVA", "destroy:" + row + "x" + col);
+        ((InstantiatedPage)o).deinstantiate(this, viewGroup);
+    }
+
+    @Override
+    public void startUpdate(ViewGroup container) {
+        // Log.d("MGVA", "startUpdate top");
+        super.startUpdate(container);
+    }
+
+    @Override
+    public void finishUpdate(ViewGroup container) {
+        // super.finishUpdate(container);
+        Log.d("MGVA", "finishUpdate bottom");
+    }
+
+    @Override
+    public void onPageScrolled(int i, int i1, float v, float v1, int i2, int i3) {
+        // Log.d("MGVA", "scrolled");
+    }
+
+    @Override
+    public void onPageSelected(int row, int col) {
+        // Log.d("MGVA", "selected");
+        final String name = pages[row][col].title;
+        mDv.post(new Runnable() {
+            @Override
+            public void run() {
+                mDv.setText(name.substring(0, Math.min(10, name.length())));
+            }
+        });
+    }
+
+    @Override
+    public void onPageScrollStateChanged(int i) {
+        ;
+    }
+}
index ebf10306a77246f6c3161d605feb366091a669d3..ff469469fd676337a230dbb9ce04b7cb86ee551f 100644 (file)
@@ -1,64 +1,83 @@
 <?xml version="1.0" encoding="utf-8"?>
-<android.support.wearable.view.BoxInsetLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
-    android:id="@+id/top_container"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     tools:context="com.acmetensortoys.watchviz.MainActivity"
-    tools:deviceIds="wear">
+    tools:deviceIds="wear"
+    android:id="@+id/outermost"
+    android:background="@color/blue">
 
-    <LinearLayout
-        android:id="@+id/linear_container"
+    <!-- Argh.  Oh boy did I lose a lot of time to this.  The GridViewPager
+    needs to be the first child in the tree so that it gets drawn first.
+    That seems hideously wrong, because it means that it's getting its size
+    information before the relative constraint solver does its thing, but
+    at this point I am exceptionally out of patience for the whole Android
+    UI layer, so here we are. -->
+
+    <!-- TODO Someone still isn't getting their background correct; the watch face
+    bleeds though around the edges of the scrolling.  Have I mentioned that I hate
+    everything ever? -->
+
+    <android.support.wearable.view.GridViewPager
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:orientation="vertical">
-
-        <RelativeLayout
-            android:orientation="horizontal"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content">
+        android:id="@+id/grid"
+        android:layout_alignParentLeft="true"
+        android:layout_marginLeft="0dp"
+        android:layout_below="@+id/text_container"
+        android:background="@color/green"
+        android:layout_alignParentBottom="true"
+        android:layout_alignParentRight="true">
+    </android.support.wearable.view.GridViewPager>
 
-            <TextView
-                android:id="@+id/text"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="@string/acmetensor"
-                app:layout_box="all"
-                android:layout_alignParentLeft="true"
-                android:layout_alignParentTop="true"
-                android:layout_alignParentStart="true" />
+    <RelativeLayout
+        android:orientation="horizontal"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/text_container"
+        android:layout_alignParentLeft="true"
+        android:layout_marginLeft="0dp"
+        android:layout_alignParentTop="true"
+        android:layout_marginTop="0dp"
+        android:background="@color/orange"
+        android:transitionGroup="false"
+        android:layout_alignParentRight="true">
 
-            <TextView
-                android:text="@string/acmetensor"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:id="@+id/dbg"
-                android:layout_alignParentTop="true"
-                android:layout_centerHorizontal="true"
-                android:layout_alignParentBottom="false"
-                android:layout_alignParentRight="false"
-                android:layout_alignParentLeft="false"
-                android:layout_alignWithParentIfMissing="false" />
+        <TextView
+            android:id="@+id/text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/acmetensor"
+            app:layout_box="all"
+            android:layout_alignParentLeft="true"
+            android:layout_alignParentTop="true"
+            android:layout_alignParentStart="true" />
 
-            <TextView
-                android:id="@+id/clock"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:textColor="@android:color/white"
-                app:layout_box="all"
-                android:layout_alignParentTop="true"
-                android:layout_alignParentEnd="true"
-                android:layout_alignParentRight="false" />
+        <TextView
+            android:text="@string/acmetensor"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:id="@+id/dbg"
+            android:layout_alignParentTop="true"
+            android:layout_centerHorizontal="true"
+            android:layout_alignParentBottom="false"
+            android:layout_alignParentRight="false"
+            android:layout_alignParentLeft="false"
+            android:layout_alignWithParentIfMissing="false" />
 
-        </RelativeLayout>
+        <TextView
+            android:id="@+id/clock"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="@android:color/white"
+            app:layout_box="all"
+            android:layout_alignParentTop="true"
+            android:layout_alignParentEnd="true"
+            android:layout_alignParentRight="false" />
 
-               <!-- Use placeholder? -->
-        <!-- <SurfaceView
-            android:layout_width="match_parent"
-            android:layout_height="fill_parent"
-            android:id="@+id/surface" /> -->
+    </RelativeLayout>
 
-    </LinearLayout>
 
-</android.support.wearable.view.BoxInsetLayout>
+</RelativeLayout>
index 0011e98942fcfe022eaa245022475353d907c45f..6fdae5852b5674742de479262caf80d038402376 100644 (file)
@@ -2,84 +2,114 @@ package com.acmetensortoys.watchviz.vizlib;
 
 import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.Rect;
 import android.util.Log;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewTreeObserver;
+
 
 public class AudioCanvas implements Renderer {
-    private AudioProvider mAudioProvider;
-    private SurfaceView mSurfaceView;
-    private Rendering rendering;
+    private final AudioProvider mAp;
+    private final SurfaceView mSv;
+    private AudioReceiver mAr;
+    private Rendering mRend;
+    private Rect gvr = new Rect();
 
-    public AudioCanvas(SurfaceView surfaceView, AudioProvider ap) {
-        this.mSurfaceView = surfaceView;
-        this.mAudioProvider = ap;
+    public AudioCanvas(final String debug1, SurfaceView sv, AudioProvider ap) {
+        this.mAp = ap;
+        this.mSv = sv;
+        sv.getHolder().addCallback(new SurfaceHolder.Callback() {
+            @Override
+            public void surfaceCreated(final SurfaceHolder h) {
+                Log.d("shc:" + debug1, "Surface Created");
+                Canvas c = h.lockCanvas();
+                c.drawColor(Color.RED);
+                h.unlockCanvasAndPost(c);
 
-        mSurfaceView.getHolder().addCallback(shc);
+                synchronized(AudioCanvas.this) {
+                    if (mAr != null) {
+                        Log.d("shc:" + debug1, "Create with existing mAr?");
+                        mAp.remove(mAr);
+                    }
+                    mAr = new AudioReceiver() {
+                        @Override
+                        public void onAudio(float[] audio, float[] fft) {
+                            final Rendering rcb;
+                            synchronized (AudioCanvas.this) {
+                                rcb = mRend;
+                                if (rcb == null) {
+                                    mAp.remove(this);
+                                }
+                            }
+                            Canvas cv = h.lockCanvas();
+                            if (cv == null) {
+                                // the surface must have been destroyed out from under us;
+                                // just stop here, on the presumption that we will be
+                                // removed from the provider by the hook below
+                                return;
+                            }
+                            cv.drawColor(Color.BLACK);
+                            rcb.render(cv, audio, fft);
+                            h.unlockCanvasAndPost(cv);
+                        }
+                    };
+                }
+            }
+            @Override
+            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+                Log.d("shc:"+debug1, "Surface Changed:"+format+":"+width+":"+height);
+                ;
+            }
+            @Override
+            public void surfaceDestroyed(SurfaceHolder holder) {
+                Log.d("shc:"+debug1, "Surface Destroyed");
+                synchronized(AudioCanvas.this) {
+                    mAp.remove(mAr);
+                    mAr = null;
+                }
+            }
+        });
+        sv.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+            @Override
+            public boolean onPreDraw() {
+                manageSubscription();
+                return true;
+            }
+        });
+        sv.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                final Rendering rcb;
+                synchronized(AudioCanvas.this) { rcb = mRend; }
+                if (rcb != null) {
+                    rcb.onClick();
+                }
+            }
+        });
     }
 
     public void setRendering(Rendering r) {
-        synchronized(this) { rendering = r; }
-    }
-
-    public void onClick() {
-        final Rendering rcb;
-        synchronized(this) { rcb = rendering; }
-        rcb.onClick();
+        synchronized(AudioCanvas.this) {
+            mRend = r;
+            manageSubscription();
+        }
     }
-
-    // The surface to which this callback is bound is created only after audio permissions
-    // have been checked.  We therefore can simply start in the created callback.  Stopping
-    // happens in onPause below, which fires before the surface is destroyed.
-    private SurfaceHolder.Callback shc = new SurfaceHolder.Callback() {
-        private AudioReceiver ar;
-
-        @Override
-        public void surfaceCreated(final SurfaceHolder h) {
-            Log.d("shc", "Surface Created");
-            Canvas c = h.lockCanvas();
-            c.drawColor(Color.RED);
-            h.unlockCanvasAndPost(c);
-
-            synchronized(this) {
-                if (ar != null) {
-                    Log.d("shc", "Create with existing ar?");
-                    mAudioProvider.remove(ar);
+    private void manageSubscription() {
+        boolean gvrf = mSv.getGlobalVisibleRect(gvr);
+        // Log.d("asv:"+debug1,"manageSubscription:gvrf="+gvrf+":gvr="+gvr.toString());
+        synchronized(AudioCanvas.this) {
+            if (mAr != null) {
+                // Surface has been created
+                if (gvrf && mRend != null) {
+                    // Visible and have rendering
+                    mAp.add(mAr);
+                } else {
+                    // Invisible or no rendering set
+                    mAp.remove(mAr);
                 }
-                ar = new AudioReceiver() {
-                    @Override
-                    public void onAudio(float[] audio, float[] fft) {
-                        final Rendering rcb;
-                        synchronized (this) { rcb = rendering; }
-                        Canvas cv = h.lockCanvas();
-                        if (cv == null) {
-                            // the surface must have been destroyed out from under us;
-                            // just stop here, on the presumption that we will be
-                            // removed from the provider by the hook below
-                            return;
-                        }
-                        cv.drawColor(Color.BLACK);
-                        rcb.render(cv, audio, fft);
-                        h.unlockCanvasAndPost(cv);
-                    }
-                };
-                mAudioProvider.add(ar);
             }
         }
-
-        @Override
-        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
-            Log.d("shc", "Surface Changed");
-            ;
-        }
-
-        @Override
-        public void surfaceDestroyed(SurfaceHolder holder) {
-            Log.d("shc", "Surface Destroyed");
-            synchronized(this) {
-                mAudioProvider.remove(ar);
-                ar = null;
-            }
-        }
-    };
+    }
 }
index fcbef8b22a630a42335e9c4e2a2af1053fd0a327..7eafa4ecf812d4065410c19795df04002e5666f9 100644 (file)
@@ -7,45 +7,55 @@ import android.util.Log;
 
 import org.jtransforms.fft.FloatFFT_1D;
 
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
 
 public class AudioProvider {
+    private static final int AUDIO_SAMPLE_HZ = 11025;
     private static final int AUDIO_RECORDER_BUFFER_SIZE = 2048;
     private static final int AUDIO_SAMPLES = 512;
 
+    // AudioReceiver callbacks run every (AUDIO_SAMPLES / AUDIO_SAMPLE_HZ) seconds.
+
     private Set<AudioReceiver> audioReceivers;
     private Thread audioSourceThread;
 
     public void add(AudioReceiver ar) {
         synchronized(this) {
             boolean empty = audioReceivers.isEmpty();
-            audioReceivers.add(ar);
+            boolean did = audioReceivers.add(ar);
 
             if (empty) {
+                Log.d("AudioProvider", "Starting audio source thread...");
                 this.start();
+            } else if (did) {
+                Log.d("AudioProvider", "Added receiver; now=" + audioReceivers.size());
             }
         }
     }
-
     public void remove(AudioReceiver ar) {
         synchronized(this) {
-            audioReceivers.remove(ar);
+            boolean did = audioReceivers.remove(ar);
+            if(!did) {
+                // Nothing to do!
+                return;
+            }
             if(audioReceivers.isEmpty()) {
+                Log.d("AudioProvider", "Stopping audio source thread...");
                 this.stop();
+            } else {
+                Log.d("AudioProvider", "Removed receiver; left=" + audioReceivers.size());
             }
         }
     }
-
     public AudioProvider() {
-        audioReceivers = Collections.synchronizedSet(new HashSet<AudioReceiver>());
+        audioReceivers = new HashSet<AudioReceiver>();
     }
 
     private void start() {
         final AudioRecord ar = new AudioRecord(
                 MediaRecorder.AudioSource.MIC,
-                11025, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_FLOAT,
+                AUDIO_SAMPLE_HZ, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_FLOAT,
                 AUDIO_RECORDER_BUFFER_SIZE);
         Log.d("shc", "Audio session ID:" + ar.getAudioSessionId());
 
@@ -65,21 +75,33 @@ public class AudioProvider {
                     System.arraycopy(samples, 0, fft, 0, AUDIO_SAMPLES);
                     fftc.realForward(fft);
 
-                    for(AudioReceiver ar : audioReceivers) {
+                    // Snapshot the current callbacks and iterate that.  This prevents
+                    // concurrent mutation exceptions, at the cost of being kind of terrible.
+                    Set<AudioReceiver> ars;
+                    synchronized(this) {
+                        ars = new HashSet<>(audioReceivers);
+                    }
+                    for (AudioReceiver ar : ars) {
                         ar.onAudio(samples, fft);
                     }
                 }
             }
         };
+        audioSourceThread.start();
     }
-
     private void stop() {
         synchronized(this) {
-            audioSourceThread.interrupt();
-
-            try { audioSourceThread.join(); }
-            catch (InterruptedException ie) { ; }
-            finally { audioSourceThread = null; }
+            if (audioSourceThread != null) {
+                audioSourceThread.interrupt();
+
+                try {
+                    audioSourceThread.join();
+                } catch (InterruptedException ie) {
+                    ;
+                } finally {
+                    audioSourceThread = null;
+                }
+            }
         }
     }
 }
index 504575065118d844a86277f7fb537e68ce1ffbe5..f59b4994524bde17c0a46a8f768610755b0b61b8 100644 (file)
@@ -4,5 +4,5 @@ import android.graphics.Canvas;
 
 public abstract class Rendering {
     abstract public void render(Canvas c, float[] audio, float[] fft);
-    public void onClick() { }
+    public void onClick() { }
 }
\ No newline at end of file
diff --git a/vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/RenderingSelector.java b/vizlib/src/main/java/com/acmetensortoys/watchviz/vizlib/RenderingSelector.java
deleted file mode 100644 (file)
index e401ca5..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-package com.acmetensortoys.watchviz.vizlib;
-
-import android.widget.TextView;
-
-import com.acmetensortoys.watchviz.vizlib.rendering.Grid;
-import com.acmetensortoys.watchviz.vizlib.rendering.WholeMax;
-
-import java.util.ArrayList;
-
-public class RenderingSelector {
-    private TextView mDebugView;
-    private Renderer mRenderer;
-
-    private int renderCBix;
-    private ArrayList<Class<? extends Rendering>> cbs = new ArrayList<>();
-
-    public RenderingSelector(TextView dv, Renderer r) {
-        mDebugView = dv;
-        mRenderer = r;
-        cbs.add(Grid.class);
-        cbs.add(WholeMax.class);
-        renderCBix = 0;
-        _setCyclerCBByIx();
-    }
-
-    private void _setCyclerCB(Class<? extends Rendering> next) {
-        Rendering rendering;
-        try {
-            rendering = next.getConstructor().newInstance();
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-        mRenderer.setRendering(rendering);
-        String name = rendering.getClass().getSimpleName();
-        mDebugView.setText(name.substring(0,Math.min(10,name.length())));
-    }
-    private void _setCyclerCBByIx() {
-        _setCyclerCB(cbs.get(renderCBix));
-    }
-    public void nextCyclerCB() {
-        synchronized(this) {
-            if (renderCBix == cbs.size()-1) {
-                renderCBix = 0;
-            } else {
-                renderCBix += 1;
-            }
-            _setCyclerCBByIx();
-        }
-    }
-    public void prevCyclerCB() {
-        synchronized(this) {
-            if (renderCBix == 0) {
-                renderCBix = cbs.size()-1;
-            } else {
-                renderCBix -= 1;
-            }
-            _setCyclerCBByIx();
-        }
-    }
-    public void setCBIx(int ix) {
-        if ((ix < 0) || (ix >= cbs.size())) {
-            throw new ArrayIndexOutOfBoundsException(ix);
-        }
-        synchronized(this) {
-            renderCBix = ix;
-            _setCyclerCBByIx();
-        }
-    }
-}