aboutsummaryrefslogtreecommitdiff
path: root/library/src/main/java/org/antennapod/audio/MediaPlayer.java
blob: 61730a4b880627ff017173ae76faa8b8078a10fd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
// Copyright 2011, Aocate, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package org.antennapod.audio;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.util.Log;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

public class MediaPlayer {

    public static final String TAG = "MediaPlayer";

    public interface OnBufferingUpdateListener {
        void onBufferingUpdate(MediaPlayer arg0, int percent);
    }

    public interface OnCompletionListener {
        void onCompletion(MediaPlayer arg0);
    }

    public interface OnErrorListener {
        boolean onError(MediaPlayer arg0, int what, int extra);
    }

    public interface OnInfoListener {
        boolean onInfo(MediaPlayer arg0, int what, int extra);
    }

    public interface OnPitchAdjustmentAvailableChangedListener {
        /**
         * @param arg0                     The owning media player
         * @param pitchAdjustmentAvailable True if pitch adjustment is available, false if not
         */
        public abstract void onPitchAdjustmentAvailableChanged(
                MediaPlayer arg0, boolean pitchAdjustmentAvailable);
    }

    public interface OnPreparedListener {
        void onPrepared(MediaPlayer arg0);
    }

    public interface OnSeekCompleteListener {
        void onSeekComplete(MediaPlayer arg0);
    }

    public interface OnSpeedAdjustmentAvailableChangedListener {
        /**
         * @param arg0                     The owning media player
         * @param speedAdjustmentAvailable True if speed adjustment is available, false if not
         */
        void onSpeedAdjustmentAvailableChanged(
                MediaPlayer arg0, boolean speedAdjustmentAvailable);
    }

    public enum State {
        IDLE, INITIALIZED, PREPARED, STARTED, PAUSED, STOPPED, PREPARING, PLAYBACK_COMPLETED, END, ERROR
    }

    private static Uri SPEED_ADJUSTMENT_MARKET_URI = Uri.parse("market://details?id=com.aocate.presto");

    private static Intent prestoMarketIntent = null;

    public static final int MEDIA_ERROR_SERVER_DIED = android.media.MediaPlayer.MEDIA_ERROR_SERVER_DIED;
    public static final int MEDIA_ERROR_UNKNOWN = android.media.MediaPlayer.MEDIA_ERROR_UNKNOWN;
    public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = android.media.MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK;

    /**
     * Indicates whether the specified action can be used as an intent. This
     * method queries the package manager for installed packages that can
     * respond to an intent with the specified action. If no suitable package is
     * found, this method returns false.
     *
     * @param context The application's environment.
     * @param action  The Intent action to check for availability.
     * @return True if an Intent with the specified action can be sent and
     * responded to, false otherwise.
     */
    public static boolean isIntentAvailable(Context context, String action) {
        final PackageManager packageManager = context.getPackageManager();
        final Intent intent = new Intent(action);
        List<ResolveInfo> list = packageManager.queryIntentServices(intent,
                PackageManager.MATCH_DEFAULT_ONLY);
        return list.size() > 0;
    }

    /**
     * Returns an explicit Intent for a service that accepts the given Intent
     * or null if no such service was found.
     *
     * @param context The application's environment.
     * @param action  The Intent action to check for availability.
     * @return The explicit service Intent or null if no service was found.
     */
    public static Intent getPrestoServiceIntent(Context context, String action) {
        final PackageManager packageManager = context.getPackageManager();
        final Intent actionIntent = new Intent(action);
        List<ResolveInfo> list = packageManager.queryIntentServices(actionIntent,
                PackageManager.MATCH_DEFAULT_ONLY);
        if (list.size() > 0) {
            ResolveInfo first = list.get(0);
            if (first.serviceInfo != null) {
                Intent intent = new Intent();
                intent.setComponent(new ComponentName(first.serviceInfo.packageName,
                        first.serviceInfo.name));
                Log.i(TAG, "Returning intent:" + intent.toString());
                return intent;
            } else {
                Log.e(TAG, "Found service that accepts " + action + ", but serviceInfo was null");
                return null;
            }
        } else {
            return null;
        }
    }

    /**
     * Indicates whether the Presto library is installed
     *
     * @param context The context to use to query the package manager.
     * @return True if the Presto library is installed, false if not.
     */
    public static boolean isPrestoLibraryInstalled(Context context) {
        return isIntentAvailable(context, ServiceBackedAudioPlayer.INTENT_NAME);
    }

    /**
     * Return an Intent that opens the Android Market page for the speed
     * alteration library
     *
     * @return The Intent for the Presto library on the Android Market
     */
    public static Intent getPrestoMarketIntent() {
        if (prestoMarketIntent == null) {
            prestoMarketIntent = new Intent(Intent.ACTION_VIEW, SPEED_ADJUSTMENT_MARKET_URI);
        }
        return prestoMarketIntent;
    }

    /**
     * Open the Android Market page for the Presto library
     *
     * @param context The context from which to open the Android Market page
     */
    public static void openPrestoMarketIntent(Context context) {
        context.startActivity(getPrestoMarketIntent());
    }

    private static final String MP_TAG = "ReplacementMediaPlayer";

    private static final double PITCH_STEP_CONSTANT = 1.0594630943593;

    private AndroidAudioPlayer amp = null;
    private ServiceBackedAudioPlayer sbmp = null;
    private SonicAudioPlayer smp = null;

    // This is whether speed adjustment should be enabled (by the Service)
    // To avoid the Service entirely, set useService to false
    protected boolean enableSpeedAdjustment = true;
    private int lastKnownPosition = 0;
    // In some cases, we're going to have to replace the
    // android.media.MediaPlayer on the fly, and we don't want to touch the
    // wrong media player, so lock it way too much.
    ReentrantLock lock = new ReentrantLock();
    private int mAudioStreamType = AudioManager.STREAM_MUSIC;
    private Context mContext;
    private boolean mIsLooping = false;
    private float mLeftVolume = 1f;
    private float mPitchStepsAdjustment = 0f;
    private float mRightVolume = 1f;
    private float mSpeedMultiplier = 1f;
    private int mWakeMode = 0;
    AbstractAudioPlayer mpi = null;
    protected boolean pitchAdjustmentAvailable = false;
    protected boolean speedAdjustmentAvailable = false;

    private Handler mServiceDisconnectedHandler = null;

    // Some parts of state cannot be found by calling MediaPlayerImpl functions,
    // so store our own state. This also helps copy state when changing
    // implementations
    State state = State.INITIALIZED;
    String stringDataSource = null;
    Uri uriDataSource = null;
    private boolean useService = false;

    // Naming Convention for Listeners
    // Most listeners can both be set by clients and called by MediaPlayImpls
    // There are a few that have to do things in this class as well as calling
    // the function. In all cases, onX is what is called by MediaPlayerImpl
    // If there is work to be done in this class, then the listener that is
    // set by setX is X (with the first letter lowercase).
    OnBufferingUpdateListener onBufferingUpdateListener = null;
    OnCompletionListener onCompletionListener = null;
    OnErrorListener onErrorListener = null;
    OnInfoListener onInfoListener = null;

    // Special case. Pitch adjustment ceases to be available when we switch
    // to the android.media.MediaPlayer (though it is not guaranteed to be
    // available when using the ServiceBackedMediaPlayer)
    OnPitchAdjustmentAvailableChangedListener onPitchAdjustmentAvailableChangedListener = new OnPitchAdjustmentAvailableChangedListener() {
        public void onPitchAdjustmentAvailableChanged(MediaPlayer arg0,
                                                      boolean pitchAdjustmentAvailable) {
            lock.lock();
            try {
                Log.d(MP_TAG, "onPitchAdjustmentAvailableChangedListener.onPitchAdjustmentAvailableChanged being called");
                if (MediaPlayer.this.pitchAdjustmentAvailable != pitchAdjustmentAvailable) {
                    Log.d(MP_TAG, "Pitch adjustment state has changed from "
                            + MediaPlayer.this.pitchAdjustmentAvailable
                            + " to " + pitchAdjustmentAvailable);
                    MediaPlayer.this.pitchAdjustmentAvailable = pitchAdjustmentAvailable;
                    if (MediaPlayer.this.pitchAdjustmentAvailableChangedListener != null) {
                        MediaPlayer.this.pitchAdjustmentAvailableChangedListener
                                .onPitchAdjustmentAvailableChanged(arg0, pitchAdjustmentAvailable);
                    }
                }
            } finally {
                lock.unlock();
            }
        }
    };
    OnPitchAdjustmentAvailableChangedListener pitchAdjustmentAvailableChangedListener = null;

    MediaPlayer.OnPreparedListener onPreparedListener = new MediaPlayer.OnPreparedListener() {
        public void onPrepared(MediaPlayer arg0) {
            Log.d(MP_TAG, "onPreparedListener 242 setting state to PREPARED");
            MediaPlayer.this.state = State.PREPARED;
            if (MediaPlayer.this.preparedListener != null) {
                Log.d(MP_TAG, "Calling preparedListener");
                MediaPlayer.this.preparedListener.onPrepared(arg0);
            }
            Log.d(MP_TAG, "Wrap up onPreparedListener");
        }
    };

    OnPreparedListener preparedListener = null;
    OnSeekCompleteListener onSeekCompleteListener = null;

    // Special case. Speed adjustment ceases to be available when we switch
    // to the android.media.MediaPlayer (though it is not guaranteed to be
    // available when using the ServiceBackedMediaPlayer)
    OnSpeedAdjustmentAvailableChangedListener onSpeedAdjustmentAvailableChangedListener = new OnSpeedAdjustmentAvailableChangedListener() {
        public void onSpeedAdjustmentAvailableChanged(MediaPlayer arg0,
                                                      boolean speedAdjustmentAvailable) {
            lock.lock();
            try {
                Log.d(MP_TAG, "onSpeedAdjustmentAvailableChangedListener.onSpeedAdjustmentAvailableChanged being called");
                if (MediaPlayer.this.speedAdjustmentAvailable != speedAdjustmentAvailable) {
                    Log.d(MP_TAG, "Speed adjustment state has changed from "
                            + MediaPlayer.this.speedAdjustmentAvailable
                            + " to " + speedAdjustmentAvailable);
                    MediaPlayer.this.speedAdjustmentAvailable = speedAdjustmentAvailable;
                    if (MediaPlayer.this.speedAdjustmentAvailableChangedListener != null) {
                        MediaPlayer.this.speedAdjustmentAvailableChangedListener
                                .onSpeedAdjustmentAvailableChanged(arg0, speedAdjustmentAvailable);
                    }
                }
            } finally {
                lock.unlock();
            }
        }
    };
    OnSpeedAdjustmentAvailableChangedListener speedAdjustmentAvailableChangedListener = null;

    public MediaPlayer(final Context context) {
        this(context, true);
    }

    public MediaPlayer(final Context context, boolean useService) {
        this.mContext = context;
        this.useService = useService;

        // So here's the major problem
        // Sometimes the service won't exist or won't be connected,
        // so start with an android.media.MediaPlayer, and when
        // the service is connected, use that from then on
        this.mpi = this.amp = new AndroidAudioPlayer(this, context);
        if(Build.VERSION.SDK_INT >= 16) {
            this.smp = new SonicAudioPlayer(this, context);
            this.smp.setDownMix(downmix());
        }

        // setupMpi will go get the Service, if it can, then bring that
        // implementation into sync
        Log.d(MP_TAG, "setupMpi");
        setupMpi(context);
    }

    protected boolean useSonic() {
        return false;
    }

    protected boolean downmix() {
        return false;
    }

    private boolean invalidServiceConnectionConfiguration() {
        if(smp != null) {
            boolean usingSonic = this.mpi instanceof SonicAudioPlayer;
            if((usingSonic && !useSonic()) || (!usingSonic && useSonic())) {
                return true;
            }
        }
        if (!(this.mpi instanceof ServiceBackedAudioPlayer)) {
            if (this.useService && isPrestoLibraryInstalled()) {
                // In this case, the Presto library has been installed
                // or something while playing sound
                // We could be using the service, but we're not
                Log.d(MP_TAG, "We could be using the service, but we're not");
                return true;
            }
            // If useService is false, then we shouldn't be using the SBMP
            // If the Presto library isn't installed, ditto
            Log.d(MP_TAG, "this.mpi is not a ServiceBackedMediaPlayer, but we couldn't use it anyway");
            return false;
        } else {
            if (BuildConfig.DEBUG && !(this.mpi instanceof ServiceBackedAudioPlayer))
                throw new AssertionError();
            if (this.useService && isPrestoLibraryInstalled()) {
                // We should be using the service, and we are. Great!
                Log.d(MP_TAG, "We could be using a ServiceBackedMediaPlayer and we are");
                return false;
            }
            // We're trying to use the service when we shouldn't,
            // that's an invalid configuration
            Log.d(MP_TAG, "We're trying to use a ServiceBackedMediaPlayer but we shouldn't be");
            return true;
        }
    }

    public boolean canFallback() {
        return this.mpi == null || false == this.mpi instanceof AndroidAudioPlayer;
    }

    public void fallback() {
        smp = null;
        setupMpi(this.mpi.mContext);
    }

    private void setupMpi(final Context context) {
        lock.lock();
        try {
            Log.d(MP_TAG, "setupMpi");
            // Check if the client wants to use the service at all,
            // then if we're already using the right kind of media player
            if(useSonic()) {
                if(mpi != null && mpi instanceof SonicAudioPlayer) {
                    Log.d(MP_TAG, "Already using SonicMediaPlayer");
                    return;
                } else {
                    Log.d(MP_TAG, "Switching to SonicMediaPlayer");
                    switchMediaPlayerImpl(mpi, smp);
                    return;
                }
            } else if (this.useService && isPrestoLibraryInstalled()) {
                if (mpi != null && mpi instanceof ServiceBackedAudioPlayer) {
                    Log.d(MP_TAG, "Already using ServiceBackedMediaPlayer");
                    return;
                }
                if (this.sbmp == null) {
                    Log.d(MP_TAG, "Instantiating new ServiceBackedMediaPlayer");
                    this.sbmp = new ServiceBackedAudioPlayer(this, context,
                            new ServiceConnection() {
                                public void onServiceConnected(ComponentName className, final IBinder service) {
                                    Thread t = new Thread(new Runnable() {
                                        @Override
                                        public void run() {
                                            // This lock probably isn't granular
                                            // enough
                                            MediaPlayer.this.lock.lock();
                                            Log.d(MP_TAG, "onServiceConnected");
                                            try {
                                                switchMediaPlayerImpl(mpi, sbmp);
                                                Log.d(MP_TAG, "End onServiceConnected");
                                            } finally {
                                                MediaPlayer.this.lock.unlock();
                                            }
                                        }
                                    });
                                    t.start();
                                }

                                public void onServiceDisconnected(ComponentName className) {
                                    MediaPlayer.this.lock.lock();
                                    try {
                                        // Can't get any more useful information
                                        // out of sbmp
                                        if (MediaPlayer.this.sbmp != null) {
                                            MediaPlayer.this.sbmp.release();
                                        }
                                        // Unlike most other cases, sbmp gets set
                                        // to null since there's nothing useful
                                        // backing it now
                                        MediaPlayer.this.sbmp = null;

                                        if (mServiceDisconnectedHandler == null) {
                                            mServiceDisconnectedHandler = new Handler(new Handler.Callback() {
                                                @Override
                                                public boolean handleMessage(Message msg) {
                                                    // switchMediaPlayerImpl won't try to
                                                    // clone anything from null
                                                    lock.lock();
                                                    try {
                                                        if (MediaPlayer.this.amp == null) {
                                                            // This should never be in this state
                                                            MediaPlayer.this.amp = new AndroidAudioPlayer(
                                                                    MediaPlayer.this,
                                                                    MediaPlayer.this.mContext);
                                                        }
                                                        // Use sbmp instead of null in case by some miracle it's
                                                        // been restored in the meantime
                                                        switchMediaPlayerImpl(mpi, amp);
                                                        return true;
                                                    } finally {
                                                        lock.unlock();
                                                    }
                                                }
                                            });
                                        }

                                        // This code needs to execute on the
                                        // original thread to instantiate
                                        // the new object in the right place
                                        mServiceDisconnectedHandler.sendMessage(
                                                mServiceDisconnectedHandler.obtainMessage());
                                        // Note that we do NOT want to set
                                        // useService. useService is about
                                        // what the user wants, not what they
                                        // get
                                    } finally {
                                        MediaPlayer.this.lock.unlock();
                                    }
                                }
                            }
                    );
                }
                Log.d(MP_TAG, "Switching to ServiceBackedMediaPlayer");
                switchMediaPlayerImpl(mpi, sbmp);
            } else {
                if (this.mpi != null && this.mpi instanceof AndroidAudioPlayer) {
                    Log.d(MP_TAG, "Already using AndroidMediaPlayer");
                    return;
                }
                if (this.amp == null) {
                    Log.d(MP_TAG, "Instantiating new AndroidMediaPlayer (this should be impossible)");
                    this.amp = new AndroidAudioPlayer(this, context);
                }
                switchMediaPlayerImpl(mpi, this.amp);
            }
        } finally {
            lock.unlock();
        }
    }

    private void switchMediaPlayerImpl(AbstractAudioPlayer from, AbstractAudioPlayer to) {
        Log.d(TAG, "switchMediaPlayerImpl() called with: " + "from = [" + from + "], to = [" + to + "]");
        lock.lock();
        try {
            Log.d(MP_TAG, "switchMediaPlayerImpl");
            if (from == to
                    // Same object, nothing to synchronize
                    || to == null
                    // Nothing to copy to (maybe this should throw an error?)
                    || (to instanceof ServiceBackedAudioPlayer && !((ServiceBackedAudioPlayer) to).isConnected())
                    // ServiceBackedMediaPlayer hasn't yet connected, onServiceConnected will take care of the transition
                    || (MediaPlayer.this.state == State.END)) {
                // State.END is after a release(), no further functions should
                // be called on this class and from is likely to have problems
                // retrieving state that won't be used anyway
                return;
            }
            // Extract all that we can from the existing implementation
            // and copy it to the new implementation

            Log.d(MP_TAG, "switchMediaPlayerImpl(), current state is " + this.state.toString());

            to.reset();

            // Do this first so we don't have to prepare the same
            // data file twice
            to.setEnableSpeedAdjustment(MediaPlayer.this.enableSpeedAdjustment);

            // This is a reasonable place to set all of these,
            // none of them require prepare() or the like first
            to.setAudioStreamType(this.mAudioStreamType);
            to.setLooping(this.mIsLooping);
            Log.d(MP_TAG, "Setting playback speed to " + this.mSpeedMultiplier);
            to.setVolume(MediaPlayer.this.mLeftVolume, MediaPlayer.this.mRightVolume);
            to.setWakeMode(this.mContext, this.mWakeMode);

            Log.d(MP_TAG, "asserting at least one data source is null");
            assert ((MediaPlayer.this.stringDataSource == null) || (MediaPlayer.this.uriDataSource == null));

            if (uriDataSource != null) {
                Log.d(MP_TAG, "switchMediaPlayerImpl(): uriDataSource != null");
                try {
                    to.setDataSource(this.mContext, uriDataSource);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (stringDataSource != null) {
                Log.d(MP_TAG, "switchMediaPlayerImpl(): stringDataSource != null");
                try {
                    to.setDataSource(stringDataSource);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            // (native) mediaplayer has to be initialized
            to.setPitchStepsAdjustment(this.mPitchStepsAdjustment);
            to.setPlaybackSpeed(this.mSpeedMultiplier);

            if ((this.state == State.PREPARED)
                    || (this.state == State.PREPARING)
                    || (this.state == State.PAUSED)
                    || (this.state == State.STOPPED)
                    || (this.state == State.STARTED)
                    || (this.state == State.PLAYBACK_COMPLETED)) {
                Log.d(MP_TAG, "switchMediaPlayerImpl(): prepare and seek");
                // Use prepare here instead of prepareAsync so that
                // we wait for it to be ready before we try to use it
                try {
                    to.muteNextOnPrepare();
                    to.prepare();
                } catch (Exception e) {
                    e.printStackTrace();
                }

                int seekPos = 0;
                if (from != null) {
                    seekPos = from.getCurrentPosition();
                } else if (this.lastKnownPosition < to.getDuration()) {
                    // This can happen if the Service unexpectedly
                    // disconnected. Because it would result in too much
                    // information being passed around, we don't constantly
                    // poll for the lastKnownPosition, but we'll save it
                    // when getCurrentPosition is called
                    seekPos = this.lastKnownPosition;
                }
                to.muteNextSeek();
                to.seekTo(seekPos);
            }
            if ((from != null)
                    && from.isPlaying()) {
                from.pause();
            }
            if ((this.state == State.STARTED)
                    || (this.state == State.PAUSED)
                    || (this.state == State.STOPPED)) {
                Log.d(MP_TAG, "switchMediaPlayerImpl(): start");
                if (to != null) {
                    to.start();
                }
            }

            if (this.state == State.PAUSED) {
                Log.d(MP_TAG, "switchMediaPlayerImpl(): paused");
                if (to != null) {
                    to.pause();
                }
            } else if (this.state == State.STOPPED) {
                Log.d(MP_TAG, "switchMediaPlayerImpl(): stopped");
                if (to != null) {
                    to.stop();
                }
            }

            this.mpi = to;
            Log.d(TAG, "Switched to " + to.getClass().toString());

            // Cheating here by relying on the side effect in
            // on(Pitch|Speed)AdjustmentAvailableChanged
            if ((to.canSetPitch() != this.pitchAdjustmentAvailable)
                    && (this.onPitchAdjustmentAvailableChangedListener != null)) {
                this.onPitchAdjustmentAvailableChangedListener
                        .onPitchAdjustmentAvailableChanged(this, to
                                .canSetPitch());
            }
            if ((to.canSetSpeed() != this.speedAdjustmentAvailable)
                    && (this.onSpeedAdjustmentAvailableChangedListener != null)) {
                this.onSpeedAdjustmentAvailableChangedListener
                        .onSpeedAdjustmentAvailableChanged(this, to
                                .canSetSpeed());
            }
            Log.d(MP_TAG, "switchMediaPlayerImpl() " + this.state.toString());
        } finally {
            lock.unlock();
        }
    }

    /**
     * Returns true if pitch can be changed at this moment
     *
     * @return True if pitch can be changed
     */
    public boolean canSetPitch() {
        lock.lock();
        try {
            return this.mpi.canSetPitch();
        } finally {
            lock.unlock();
        }
    }

    /**
     * Returns true if speed can be changed at this moment
     *
     * @return True if speed can be changed
     */
    public boolean canSetSpeed() {
        lock.lock();
        try {
            return this.mpi.canSetSpeed();
        } finally {
            lock.unlock();
        }
    }

    public boolean canDownmix() {
        lock.lock();
        try {
            return this.mpi.canDownmix();
        } finally {
            lock.unlock();
        }
    }

    protected void finalize() throws Throwable {
        lock.lock();
        try {
            Log.d(MP_TAG, "finalize()");
            this.release();
        } finally {
            lock.unlock();
        }
    }

    /**
     * Returns the number of steps (in a musical scale) by which playback is
     * currently shifted. When greater than zero, pitch is shifted up. When less
     * than zero, pitch is shifted down.
     *
     * @return The number of steps pitch is currently shifted by
     */
    public float getCurrentPitchStepsAdjustment() {
        lock.lock();
        try {
            return this.mpi.getCurrentPitchStepsAdjustment();
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to android.media.MediaPlayer.getCurrentPosition()
     * Accurate only to frame size of encoded data (26 ms for MP3s)
     *
     * @return Current position (in milliseconds)
     */
    public int getCurrentPosition() {
        lock.lock();
        try {
            return (this.lastKnownPosition = this.mpi.getCurrentPosition());
        } finally {
            lock.unlock();
        }
    }

    /**
     * Returns the current speed multiplier. Defaults to 1.0 (normal speed)
     *
     * @return The current speed multiplier
     */
    public float getCurrentSpeedMultiplier() {
        lock.lock();
        try {
            return this.mpi.getCurrentSpeedMultiplier();
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to android.media.MediaPlayer.getDuration()
     *
     * @return Length of the track (in milliseconds)
     */
    public int getDuration() {
        lock.lock();
        try {
            return this.mpi.getDuration();
        } finally {
            lock.unlock();
        }
    }

    /**
     * Get the maximum value that can be passed to setPlaybackSpeed
     *
     * @return The maximum speed multiplier
     */
    public float getMaxSpeedMultiplier() {
        lock.lock();
        try {
            return this.mpi.getMaxSpeedMultiplier();
        } finally {
            lock.unlock();
        }
    }

    /**
     * Get the minimum value that can be passed to setPlaybackSpeed
     *
     * @return The minimum speed multiplier
     */
    public float getMinSpeedMultiplier() {
        lock.lock();
        try {
            return this.mpi.getMinSpeedMultiplier();
        } finally {
            lock.unlock();
        }
    }

    /**
     * Gets the version code of the backing service
     *
     * @return -1 if ServiceBackedMediaPlayer is not used, 0 if the service is not
     * connected, otherwise the version code retrieved from the service
     */
    public int getServiceVersionCode() {
        lock.lock();
        try {
            if (this.mpi instanceof ServiceBackedAudioPlayer) {
                return ((ServiceBackedAudioPlayer) this.mpi).getServiceVersionCode();
            } else {
                return -1;
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * Gets the version name of the backing service
     *
     * @return null if ServiceBackedMediaPlayer is not used, empty string if
     * the service is not connected, otherwise the version name retrieved from
     * the service
     */
    public String getServiceVersionName() {
        lock.lock();
        try {
            if (this.mpi instanceof ServiceBackedAudioPlayer) {
                return ((ServiceBackedAudioPlayer) this.mpi).getServiceVersionName();
            } else {
                return null;
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to android.media.MediaPlayer.isLooping()
     *
     * @return True if the track is looping
     */
    public boolean isLooping() {
        lock.lock();
        try {
            return this.mpi.isLooping();
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to android.media.MediaPlayer.isPlaying()
     *
     * @return True if the track is playing
     */
    public boolean isPlaying() {
        lock.lock();
        try {
            return this.mpi.isPlaying();
        } finally {
            lock.unlock();
        }
    }

    /**
     * Returns true if this MediaPlayer has access to the Presto
     * library
     *
     * @return True if the Presto library is installed
     */
    public boolean isPrestoLibraryInstalled() {
        if ((this.mpi == null) || (this.mpi.mContext == null)) {
            return false;
        }
        return isPrestoLibraryInstalled(this.mpi.mContext);
    }

    /**
     * Open the Android Market page in the same context as this MediaPlayer
     */
    public void openPrestoMarketIntent() {
        if ((this.mpi != null) && (this.mpi.mContext != null)) {
            openPrestoMarketIntent(this.mpi.mContext);
        }
    }

    /**
     * Functions identically to android.media.MediaPlayer.pause() Pauses the
     * track
     */
    public void pause() {
        lock.lock();
        try {
            if (invalidServiceConnectionConfiguration()) {
                setupMpi(this.mpi.mContext);
            }
            this.state = State.PAUSED;
            this.mpi.pause();
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to android.media.MediaPlayer.prepare() Prepares the
     * track. This or prepareAsync must be called before start()
     */
    public void prepare() throws IllegalStateException, IOException {
        lock.lock();
        try {
            Log.d(MP_TAG, "prepare() using " + ((this.mpi == null) ? "null (this shouldn't happen)" : this.mpi.getClass().toString()) + " state " + this.state.toString());
            Log.d(MP_TAG, "onPreparedListener is: " + ((this.onPreparedListener == null) ? "null" : "non-null"));
            Log.d(MP_TAG, "preparedListener is: " + ((this.preparedListener == null) ? "null" : "non-null"));
            if (invalidServiceConnectionConfiguration()) {
                setupMpi(this.mpi.mContext);
            }
            this.mpi.prepare();
            this.state = State.PREPARED;
            Log.d(MP_TAG, "prepare() finished");
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to android.media.MediaPlayer.prepareAsync()
     * Prepares the track. This or prepare must be called before start()
     */
    public void prepareAsync() {
        lock.lock();
        try {
            Log.d(MP_TAG, "prepareAsync()");
            if (invalidServiceConnectionConfiguration()) {
                setupMpi(this.mpi.mContext);
            }
            this.state = State.PREPARING;
            this.mpi.prepareAsync();
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to android.media.MediaPlayer.release() Releases the
     * underlying resources used by the media player.
     */
    public void release() {
        lock.lock();
        try {
            Log.d(MP_TAG, "Releasing MediaPlayer");

            this.state = State.END;
            if (this.amp != null) {
                this.amp.release();
            }
            if (this.sbmp != null) {
                this.sbmp.release();
            }

            this.onBufferingUpdateListener = null;
            this.onCompletionListener = null;
            this.onErrorListener = null;
            this.onInfoListener = null;
            this.preparedListener = null;
            this.onPitchAdjustmentAvailableChangedListener = null;
            this.pitchAdjustmentAvailableChangedListener = null;
            Log.d(MP_TAG, "Setting onSeekCompleteListener to null 871");
            this.onSeekCompleteListener = null;
            this.onSpeedAdjustmentAvailableChangedListener = null;
            this.speedAdjustmentAvailableChangedListener = null;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to android.media.MediaPlayer.reset() Resets the
     * track to idle state
     */
    public void reset() {
        lock.lock();
        try {
            this.state = State.IDLE;
            this.stringDataSource = null;
            this.uriDataSource = null;
            this.mpi.reset();
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to android.media.MediaPlayer.seekTo(int msec) Seeks
     * to msec in the track
     */
    public void seekTo(int msec) throws IllegalStateException {
        lock.lock();
        try {
            this.mpi.seekTo(msec);
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to android.media.MediaPlayer.setAudioStreamType(int
     * streamtype) Sets the audio stream type.
     */
    public void setAudioStreamType(int streamtype) {
        lock.lock();
        try {
            this.mAudioStreamType = streamtype;
            this.mpi.setAudioStreamType(streamtype);
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to android.media.MediaPlayer.setDataSource(Context
     * context, Uri uri) Sets uri as data source in the context given
     */
    public void setDataSource(Context context, Uri uri)
            throws IllegalArgumentException, IllegalStateException, IOException {
        lock.lock();
        try {
            Log.d(MP_TAG, "In setDataSource(context, " + uri.toString() + "), using " + this.mpi.getClass().toString());
            if (invalidServiceConnectionConfiguration()) {
                setupMpi(this.mpi.mContext);
            }
            this.state = State.INITIALIZED;
            this.stringDataSource = null;
            this.uriDataSource = uri;
            this.mpi.setDataSource(context, uri);
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to android.media.MediaPlayer.setDataSource(String
     * path) Sets the data source of the track to a file given.
     */
    public void setDataSource(String path) throws IllegalArgumentException,
            IllegalStateException, IOException {
        lock.lock();
        try {
            Log.d(MP_TAG, "In setDataSource(context, " + path + ")");
            if (invalidServiceConnectionConfiguration()) {
                setupMpi(this.mpi.mContext);
            }
            this.state = State.INITIALIZED;
            this.stringDataSource = path;
            this.uriDataSource = null;
            this.mpi.setDataSource(path);
        } finally {
            lock.unlock();
        }
    }

    /**
     * Sets whether to use speed adjustment or not. Speed adjustment on is more
     * computation-intensive than with it off.
     *
     * @param enableSpeedAdjustment Whether speed adjustment should be supported.
     */
    public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) {
        lock.lock();
        try {
            this.enableSpeedAdjustment = enableSpeedAdjustment;
            this.mpi.setEnableSpeedAdjustment(enableSpeedAdjustment);
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to android.media.MediaPlayer.setLooping(boolean
     * loop) Sets the track to loop infinitely if loop is true, play once if
     * loop is false
     */
    public void setLooping(boolean loop) {
        lock.lock();
        try {
            this.mIsLooping = loop;
            this.mpi.setLooping(loop);
        } finally {
            lock.unlock();
        }
    }

    /**
     * Sets the number of steps (in a musical scale) by which playback is
     * currently shifted. When greater than zero, pitch is shifted up. When less
     * than zero, pitch is shifted down.
     *
     * @param pitchSteps The number of steps by which to shift playback
     */
    public void setPitchStepsAdjustment(float pitchSteps) {
        lock.lock();
        try {
            this.mPitchStepsAdjustment = pitchSteps;
            this.mpi.setPitchStepsAdjustment(pitchSteps);
        } finally {
            lock.unlock();
        }
    }

    private static float getPitchStepsAdjustment(float pitch) {
        return (float) (Math.log(pitch) / (2 * Math.log(PITCH_STEP_CONSTANT)));
    }

    /**
     * Sets the percentage by which pitch is currently shifted. When greater
     * than zero, pitch is shifted up. When less than zero, pitch is shifted
     * down
     *
     * @param pitch The percentage to shift pitch
     */
    public void setPlaybackPitch(float pitch) {
        lock.lock();
        try {
            this.mPitchStepsAdjustment = getPitchStepsAdjustment(pitch);
            this.mpi.setPlaybackPitch(pitch);
        } finally {
            lock.unlock();
        }
    }

    /**
     * Set playback speed. 1.0 is normal speed, 2.0 is double speed, and so on.
     * Speed should never be set to 0 or below.
     *
     * @param f The speed multiplier to use for further playback
     */
    public void setPlaybackSpeed(float f) {
        lock.lock();
        try {
            this.mSpeedMultiplier = f;
            this.mpi.setPlaybackSpeed(f);
        } finally {
            lock.unlock();
        }
    }

    public void setDownmix(boolean enable) {
        lock.lock();
        try {
            this.mpi.setDownmix(enable);
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to android.media.MediaPlayer.setVolume(float
     * leftVolume, float rightVolume) Sets the stereo volume
     */
    public void setVolume(float leftVolume, float rightVolume) {
        lock.lock();
        try {
            this.mLeftVolume = leftVolume;
            this.mRightVolume = rightVolume;
            this.mpi.setVolume(leftVolume, rightVolume);
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to android.media.MediaPlayer.setWakeMode(Context
     * context, int mode) Acquires a wake lock in the context given. You must
     * request the appropriate permissions in your AndroidManifest.xml file.
     */
    public void setWakeMode(Context context, int mode) {
        lock.lock();
        try {
            this.mWakeMode = mode;
            this.mpi.setWakeMode(context, mode);
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to
     * android.media.MediaPlayer.setOnCompletionListener(OnCompletionListener
     * listener) Sets a listener to be used when a track completes playing.
     */
    public void setOnBufferingUpdateListener(OnBufferingUpdateListener listener) {
        lock.lock();
        try {
            this.onBufferingUpdateListener = listener;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to
     * android.media.MediaPlayer.setOnCompletionListener(OnCompletionListener
     * listener) Sets a listener to be used when a track completes playing.
     */
    public void setOnCompletionListener(OnCompletionListener listener) {
        lock.lock();
        try {
            this.onCompletionListener = listener;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to
     * android.media.MediaPlayer.setOnErrorListener(OnErrorListener listener)
     * Sets a listener to be used when a track encounters an error.
     */
    public void setOnErrorListener(OnErrorListener listener) {
        lock.lock();
        try {
            this.onErrorListener = listener;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to
     * android.media.MediaPlayer.setOnInfoListener(OnInfoListener listener) Sets
     * a listener to be used when a track has info.
     */
    public void setOnInfoListener(OnInfoListener listener) {
        lock.lock();
        try {
            this.onInfoListener = listener;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Sets a listener that will fire when pitch adjustment becomes available or
     * stops being available
     */
    public void setOnPitchAdjustmentAvailableChangedListener(
            OnPitchAdjustmentAvailableChangedListener listener) {
        lock.lock();
        try {
            this.pitchAdjustmentAvailableChangedListener = listener;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to
     * android.media.MediaPlayer.setOnPreparedListener(OnPreparedListener
     * listener) Sets a listener to be used when a track finishes preparing.
     */
    public void setOnPreparedListener(OnPreparedListener listener) {
        lock.lock();
        Log.d(MP_TAG, " ++++++++++++++++++++++++++++++++++++++++++++ setOnPreparedListener");
        try {
            this.preparedListener = listener;
            // For this one, we do not explicitly set the MediaPlayer or the
            // Service listener. This is because in addition to calling the
            // listener provided by the client, it's necessary to change
            // state to PREPARED. See prepareAsync for implementation details
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to
     * android.media.MediaPlayer.setOnSeekCompleteListener
     * (OnSeekCompleteListener listener) Sets a listener to be used when a track
     * finishes seeking.
     */
    public void setOnSeekCompleteListener(OnSeekCompleteListener listener) {
        lock.lock();
        try {
            this.onSeekCompleteListener = listener;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Sets a listener that will fire when speed adjustment becomes available or
     * stops being available
     */
    public void setOnSpeedAdjustmentAvailableChangedListener(
            OnSpeedAdjustmentAvailableChangedListener listener) {
        lock.lock();
        try {
            this.speedAdjustmentAvailableChangedListener = listener;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to android.media.MediaPlayer.start() Starts a track
     * playing
     */
    public void start() {
        lock.lock();
        try {
            Log.d(MP_TAG, "start()");
            if (invalidServiceConnectionConfiguration()) {
                setupMpi(this.mpi.mContext);
            }
            this.state = State.STARTED;
            this.mpi.start();
        } finally {
            lock.unlock();
        }
    }

    /**
     * Functions identically to android.media.MediaPlayer.stop() Stops a track
     * playing and resets its position to the start.
     */
    public void stop() {
        lock.lock();
        try {
            if (invalidServiceConnectionConfiguration()) {
                setupMpi(this.mpi.mContext);
            }
            this.state = State.STOPPED;
            this.mpi.stop();
        } finally {
            lock.unlock();
        }
    }

}