Updates Wear Speaker with latest APIs and move AsyncTask to static to
avoid memory leaks.

Bug: 4368433
Test: Manual tests.
Change-Id: Id4d837dddd82efdfa3726e5a759ab49ac47c0eff
diff --git a/wearable/wear/WearSpeakerSample/build.gradle b/wearable/wear/WearSpeakerSample/build.gradle
index 6c111e7..b35d701 100644
--- a/wearable/wear/WearSpeakerSample/build.gradle
+++ b/wearable/wear/WearSpeakerSample/build.gradle
@@ -22,7 +22,7 @@
         google()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.0.1'
+        classpath 'com.android.tools.build:gradle:3.1.3'
 
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
diff --git a/wearable/wear/WearSpeakerSample/wear/build.gradle b/wearable/wear/WearSpeakerSample/wear/build.gradle
index 19604a1..08c92f4 100644
--- a/wearable/wear/WearSpeakerSample/wear/build.gradle
+++ b/wearable/wear/WearSpeakerSample/wear/build.gradle
@@ -19,7 +19,7 @@
 
 android {
     compileSdkVersion 26
-    buildToolsVersion '26.0.2'
+    buildToolsVersion '27.0.3'
 
     defaultConfig {
         applicationId "com.example.android.wearable.speaker"
@@ -38,11 +38,16 @@
 
 dependencies {
 
-    compile 'com.android.support:wear:27.1.0'
+    implementation 'com.android.support:wear:27.1.1'
+    implementation 'com.android.support:animated-vector-drawable:27.1.1'
+    implementation 'com.android.support:support-media-compat:27.1.1'
 
-    compile 'com.google.android.gms:play-services-wearable:11.8.0'
-    compile 'com.android.support:appcompat-v7:27.1.0'
+    implementation 'com.android.support:percent:27.1.1'
 
-    provided 'com.google.android.wearable:wearable:2.3.0'
-    compile 'com.google.android.support:wearable:2.3.0'
+    implementation 'com.google.android.gms:play-services-wearable:15.0.1'
+    implementation 'com.android.support:appcompat-v7:27.1.1'
+    implementation 'com.android.support:support-v4:27.1.1'
+
+    compileOnly 'com.google.android.wearable:wearable:2.3.0'
+    implementation 'com.google.android.support:wearable:2.3.0'
 }
diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/java/com/example/android/wearable/speaker/MainActivity.java b/wearable/wear/WearSpeakerSample/wear/src/main/java/com/example/android/wearable/speaker/MainActivity.java
index e212195..72813b4 100644
--- a/wearable/wear/WearSpeakerSample/wear/src/main/java/com/example/android/wearable/speaker/MainActivity.java
+++ b/wearable/wear/WearSpeakerSample/wear/src/main/java/com/example/android/wearable/speaker/MainActivity.java
@@ -17,7 +17,6 @@
 package com.example.android.wearable.speaker;
 
 import android.Manifest;
-import android.app.Activity;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
@@ -28,8 +27,9 @@
 import android.os.Bundle;
 import android.os.CountDownTimer;
 import android.support.v4.app.ActivityCompat;
+import android.support.v4.app.FragmentActivity;
 import android.support.v4.content.ContextCompat;
-import android.support.wear.ambient.AmbientMode;
+import android.support.wear.ambient.AmbientModeSupport;
 import android.util.Log;
 import android.view.View;
 import android.widget.ImageView;
@@ -45,8 +45,8 @@
  * to 10 seconds), a Play icon (if clicked, it wil playback the recorded audio file) and a music
  * note icon (if clicked, it plays an MP3 file that is included in the app).
  */
-public class MainActivity extends Activity implements
-        AmbientMode.AmbientCallbackProvider,
+public class MainActivity extends FragmentActivity implements
+        AmbientModeSupport.AmbientCallbackProvider,
         UIAnimation.UIStateListener,
         SoundRecorder.OnVoicePlaybackStateChangedListener {
 
@@ -72,7 +72,7 @@
      * Ambient mode controller attached to this display. Used by Activity to see if it is in
      * ambient mode.
      */
-    private AmbientMode.AmbientController mAmbientController;
+    private AmbientModeSupport.AmbientController mAmbientController;
 
     enum AppState {
         READY, PLAYING_VOICE, PLAYING_MUSIC, RECORDING
@@ -89,7 +89,7 @@
         mProgressBar = findViewById(R.id.progress_bar);
 
         // Enables Ambient mode.
-        mAmbientController = AmbientMode.attachAmbientSupport(this);
+        mAmbientController = AmbientModeSupport.attach(this);
     }
 
     private void setProgressBar(long progressInMillis) {
@@ -239,10 +239,10 @@
         int[] thumbResources = new int[] {R.id.mic, R.id.play, R.id.music};
         ImageView[] thumbs = new ImageView[3];
         for(int i=0; i < 3; i++) {
-            thumbs[i] = (ImageView) findViewById(thumbResources[i]);
+            thumbs[i] = findViewById(thumbResources[i]);
         }
         View containerView = findViewById(R.id.container);
-        ImageView expandedView = (ImageView) findViewById(R.id.expanded);
+        ImageView expandedView = findViewById(R.id.expanded);
         int animationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime);
         mUIAnimation = new UIAnimation(containerView, thumbs, expandedView, animationDuration,
                 this);
@@ -312,11 +312,11 @@
     }
 
     @Override
-    public AmbientMode.AmbientCallback getAmbientCallback() {
+    public AmbientModeSupport.AmbientCallback getAmbientCallback() {
         return new MyAmbientCallback();
     }
 
-    private class MyAmbientCallback extends AmbientMode.AmbientCallback {
+    private class MyAmbientCallback extends AmbientModeSupport.AmbientCallback {
         /** Prepares the UI for ambient mode. */
         @Override
         public void onEnterAmbient(Bundle ambientDetails) {
diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/java/com/example/android/wearable/speaker/SoundRecorder.java b/wearable/wear/WearSpeakerSample/wear/src/main/java/com/example/android/wearable/speaker/SoundRecorder.java
index a45bdd2..63604b0 100644
--- a/wearable/wear/WearSpeakerSample/wear/src/main/java/com/example/android/wearable/speaker/SoundRecorder.java
+++ b/wearable/wear/WearSpeakerSample/wear/src/main/java/com/example/android/wearable/speaker/SoundRecorder.java
@@ -32,6 +32,7 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
+import java.lang.ref.WeakReference;
 
 /**
  * A helper class to provide methods to record audio input from the MIC to the internal storage
@@ -79,62 +80,7 @@
             return;
         }
 
-        mRecordingAsyncTask = new AsyncTask<Void, Void, Void>() {
-
-            private AudioRecord mAudioRecord;
-
-            @Override
-            protected void onPreExecute() {
-                mState = State.RECORDING;
-            }
-
-            @Override
-            protected Void doInBackground(Void... params) {
-                mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
-                        RECORDING_RATE, CHANNEL_IN, FORMAT, BUFFER_SIZE * 3);
-                BufferedOutputStream bufferedOutputStream = null;
-                try {
-                    bufferedOutputStream = new BufferedOutputStream(
-                            mContext.openFileOutput(mOutputFileName, Context.MODE_PRIVATE));
-                    byte[] buffer = new byte[BUFFER_SIZE];
-                    mAudioRecord.startRecording();
-                    while (!isCancelled()) {
-                        int read = mAudioRecord.read(buffer, 0, buffer.length);
-                        bufferedOutputStream.write(buffer, 0, read);
-                    }
-                } catch (IOException | NullPointerException | IndexOutOfBoundsException e) {
-                    Log.e(TAG, "Failed to record data: " + e);
-                } finally {
-                    if (bufferedOutputStream != null) {
-                        try {
-                            bufferedOutputStream.close();
-                        } catch (IOException e) {
-                            // ignore
-                        }
-                    }
-                    mAudioRecord.release();
-                    mAudioRecord = null;
-                }
-                return null;
-            }
-
-            @Override
-            protected void onPostExecute(Void aVoid) {
-                mState = State.IDLE;
-                mRecordingAsyncTask = null;
-            }
-
-            @Override
-            protected void onCancelled() {
-                if (mState == State.RECORDING) {
-                    Log.d(TAG, "Stopping the recording ...");
-                    mState = State.IDLE;
-                } else {
-                    Log.w(TAG, "Requesting to stop recording while state was not RECORDING");
-                }
-                mRecordingAsyncTask = null;
-            }
-        };
+        mRecordingAsyncTask = new RecordAudioAsyncTask(this);
 
         mRecordingAsyncTask.execute();
     }
@@ -172,74 +118,9 @@
             }
             return;
         }
-        final int intSize = AudioTrack.getMinBufferSize(RECORDING_RATE, CHANNELS_OUT, FORMAT);
+        int intSize = AudioTrack.getMinBufferSize(RECORDING_RATE, CHANNELS_OUT, FORMAT);
 
-        mPlayingAsyncTask = new AsyncTask<Void, Void, Void>() {
-
-            private AudioTrack mAudioTrack;
-
-            @Override
-            protected void onPreExecute() {
-                mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC,
-                        mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC), 0 /* flags */);
-                mState = State.PLAYING;
-            }
-
-            @Override
-            protected Void doInBackground(Void... params) {
-                try {
-                    mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, RECORDING_RATE,
-                            CHANNELS_OUT, FORMAT, intSize, AudioTrack.MODE_STREAM);
-                    byte[] buffer = new byte[intSize * 2];
-                    FileInputStream in = null;
-                    BufferedInputStream bis = null;
-                    mAudioTrack.setVolume(AudioTrack.getMaxVolume());
-                    mAudioTrack.play();
-                    try {
-                        in = mContext.openFileInput(mOutputFileName);
-                        bis = new BufferedInputStream(in);
-                        int read;
-                        while (!isCancelled() && (read = bis.read(buffer, 0, buffer.length)) > 0) {
-                            mAudioTrack.write(buffer, 0, read);
-                        }
-                    } catch (IOException e) {
-                        Log.e(TAG, "Failed to read the sound file into a byte array", e);
-                    } finally {
-                        try {
-                            if (in != null) {
-                                in.close();
-                            }
-                            if (bis != null) {
-                                bis.close();
-                            }
-                        } catch (IOException e) { /* ignore */}
-
-                        mAudioTrack.release();
-                    }
-                } catch (IllegalStateException e) {
-                    Log.e(TAG, "Failed to start playback", e);
-                }
-                return null;
-            }
-
-            @Override
-            protected void onPostExecute(Void aVoid) {
-                cleanup();
-            }
-
-            @Override
-            protected void onCancelled() {
-                cleanup();
-            }
-
-            private void cleanup() {
-                if (mListener != null) {
-                    mListener.onPlaybackStopped();
-                }
-                mState = State.IDLE;
-                mPlayingAsyncTask = null;
-            }
-        };
+        mPlayingAsyncTask = new PlayAudioAsyncTask(this, intSize);
 
         mPlayingAsyncTask.execute();
     }
@@ -260,4 +141,176 @@
         stopPlaying();
         stopRecording();
     }
+
+
+    private static class PlayAudioAsyncTask extends AsyncTask<Void, Void, Void> {
+
+        private WeakReference<SoundRecorder> mSoundRecorderWeakReference;
+
+        private AudioTrack mAudioTrack;
+        private int mIntSize;
+
+        PlayAudioAsyncTask(SoundRecorder context, int intSize) {
+            mSoundRecorderWeakReference = new WeakReference<>(context);
+            mIntSize = intSize;
+        }
+
+        @Override
+        protected void onPreExecute() {
+
+            SoundRecorder soundRecorder = mSoundRecorderWeakReference.get();
+
+            if (soundRecorder != null) {
+                soundRecorder.mAudioManager.setStreamVolume(
+                        AudioManager.STREAM_MUSIC,
+                        soundRecorder.mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC),
+                        0 /* flags */);
+                soundRecorder.mState = State.PLAYING;
+            }
+        }
+
+        @Override
+        protected Void doInBackground(Void... params) {
+            SoundRecorder soundRecorder = mSoundRecorderWeakReference.get();
+
+            try {
+                mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, RECORDING_RATE,
+                        CHANNELS_OUT, FORMAT, mIntSize, AudioTrack.MODE_STREAM);
+                byte[] buffer = new byte[mIntSize * 2];
+                FileInputStream in = null;
+                BufferedInputStream bis = null;
+                mAudioTrack.setVolume(AudioTrack.getMaxVolume());
+                mAudioTrack.play();
+                try {
+                    in = soundRecorder.mContext.openFileInput(soundRecorder.mOutputFileName);
+                    bis = new BufferedInputStream(in);
+                    int read;
+                    while (!isCancelled() && (read = bis.read(buffer, 0, buffer.length)) > 0) {
+                        mAudioTrack.write(buffer, 0, read);
+                    }
+                } catch (IOException e) {
+                    Log.e(TAG, "Failed to read the sound file into a byte array", e);
+                } finally {
+                    try {
+                        if (in != null) {
+                            in.close();
+                        }
+                        if (bis != null) {
+                            bis.close();
+                        }
+                    } catch (IOException e) { /* ignore */}
+
+                    mAudioTrack.release();
+                }
+            } catch (IllegalStateException e) {
+                Log.e(TAG, "Failed to start playback", e);
+            }
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Void aVoid) {
+            cleanup();
+        }
+
+        @Override
+        protected void onCancelled() {
+            cleanup();
+        }
+
+        private void cleanup() {
+            SoundRecorder soundRecorder = mSoundRecorderWeakReference.get();
+
+            if (soundRecorder != null) {
+                if (soundRecorder.mListener != null) {
+                    soundRecorder.mListener.onPlaybackStopped();
+                }
+                soundRecorder.mState = State.IDLE;
+                soundRecorder.mPlayingAsyncTask = null;
+            }
+        }
+    }
+
+    private static class RecordAudioAsyncTask extends AsyncTask<Void, Void, Void> {
+
+        private WeakReference<SoundRecorder> mSoundRecorderWeakReference;
+
+        private AudioRecord mAudioRecord;
+
+        RecordAudioAsyncTask(SoundRecorder context) {
+            mSoundRecorderWeakReference = new WeakReference<>(context);
+        }
+
+        @Override
+        protected void onPreExecute() {
+            SoundRecorder soundRecorder = mSoundRecorderWeakReference.get();
+
+            if (soundRecorder != null) {
+                soundRecorder.mState = State.RECORDING;
+            }
+        }
+
+        @Override
+        protected Void doInBackground(Void... params) {
+
+            SoundRecorder soundRecorder = mSoundRecorderWeakReference.get();
+
+            mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
+                    RECORDING_RATE, CHANNEL_IN, FORMAT, BUFFER_SIZE * 3);
+
+
+            BufferedOutputStream bufferedOutputStream = null;
+
+            try {
+                bufferedOutputStream = new BufferedOutputStream(
+                        soundRecorder.mContext.openFileOutput(
+                                soundRecorder.mOutputFileName,
+                                Context.MODE_PRIVATE));
+                byte[] buffer = new byte[BUFFER_SIZE];
+                mAudioRecord.startRecording();
+                while (!isCancelled()) {
+                    int read = mAudioRecord.read(buffer, 0, buffer.length);
+                    bufferedOutputStream.write(buffer, 0, read);
+                }
+            } catch (IOException | NullPointerException | IndexOutOfBoundsException e) {
+                Log.e(TAG, "Failed to record data: " + e);
+            } finally {
+                if (bufferedOutputStream != null) {
+                    try {
+                        bufferedOutputStream.close();
+                    } catch (IOException e) {
+                        // ignore
+                    }
+                }
+                mAudioRecord.release();
+                mAudioRecord = null;
+            }
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Void aVoid) {
+            SoundRecorder soundRecorder = mSoundRecorderWeakReference.get();
+
+            if (soundRecorder != null) {
+                soundRecorder.mState = State.IDLE;
+                soundRecorder.mRecordingAsyncTask = null;
+            }
+        }
+
+        @Override
+        protected void onCancelled() {
+            SoundRecorder soundRecorder = mSoundRecorderWeakReference.get();
+
+            if (soundRecorder != null) {
+                if (soundRecorder.mState == State.RECORDING) {
+                    Log.d(TAG, "Stopping the recording ...");
+                    soundRecorder.mState = State.IDLE;
+                } else {
+                    Log.w(TAG, "Requesting to stop recording while state was not RECORDING");
+                }
+                soundRecorder.mRecordingAsyncTask = null;
+            }
+        }
+    }
 }