• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.pump.db;
18 
19 import android.content.ContentResolver;
20 import android.content.ContentValues;
21 import android.database.ContentObserver;
22 import android.database.Cursor;
23 import android.net.Uri;
24 import android.provider.MediaStore;
25 
26 import androidx.annotation.AnyThread;
27 import androidx.annotation.NonNull;
28 import androidx.annotation.Nullable;
29 import androidx.annotation.WorkerThread;
30 
31 import com.android.pump.util.Clog;
32 import com.android.pump.util.Collections;
33 
34 import java.io.File;
35 import java.util.ArrayList;
36 import java.util.Collection;
37 
38 @WorkerThread
39 class AudioStore extends ContentObserver {
40     private static final String TAG = Clog.tag(AudioStore.class);
41 
42     private final ContentResolver mContentResolver;
43     private final ChangeListener mChangeListener;
44     private final MediaProvider mMediaProvider;
45 
46     interface ChangeListener {
onAudiosAdded(@onNull Collection<Audio> audios)47         void onAudiosAdded(@NonNull Collection<Audio> audios);
onArtistsAdded(@onNull Collection<Artist> artists)48         void onArtistsAdded(@NonNull Collection<Artist> artists);
onAlbumsAdded(@onNull Collection<Album> albums)49         void onAlbumsAdded(@NonNull Collection<Album> albums);
onGenresAdded(@onNull Collection<Genre> genres)50         void onGenresAdded(@NonNull Collection<Genre> genres);
onPlaylistsAdded(@onNull Collection<Playlist> playlists)51         void onPlaylistsAdded(@NonNull Collection<Playlist> playlists);
52     }
53 
54     @AnyThread
AudioStore(@onNull ContentResolver contentResolver, @NonNull ChangeListener changeListener, @NonNull MediaProvider mediaProvider)55     AudioStore(@NonNull ContentResolver contentResolver, @NonNull ChangeListener changeListener,
56             @NonNull MediaProvider mediaProvider) {
57         super(null);
58 
59         Clog.i(TAG, "AudioStore(" + contentResolver + ", " + changeListener
60                 + ", " + mediaProvider + ")");
61         mContentResolver = contentResolver;
62         mChangeListener = changeListener;
63         mMediaProvider = mediaProvider;
64 
65         // TODO(123705758) Do we need content observer for other content uris? (E.g. album, artist)
66         mContentResolver.registerContentObserver(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
67                 true, this);
68 
69         // TODO(123705758) When to call unregisterContentObserver?
70         // mContentResolver.unregisterContentObserver(this);
71     }
72 
load()73     void load() {
74         Clog.i(TAG, "load()");
75         ArrayList<Artist> artists = new ArrayList<>();
76         ArrayList<Album> albums = new ArrayList<>();
77         ArrayList<Audio> audios = new ArrayList<>();
78         ArrayList<Playlist> playlists = new ArrayList<>();
79         ArrayList<Genre> genres = new ArrayList<>();
80 
81         // #1 Load artists
82         {
83             Uri contentUri = MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI;
84             String[] projection = {
85                 MediaStore.Audio.Artists._ID
86             };
87             String sortOrder = MediaStore.Audio.Artists._ID;
88             Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder);
89             if (cursor != null) {
90                 try {
91                     int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Artists._ID);
92 
93                     for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
94                         long id = cursor.getLong(idColumn);
95 
96                         Artist artist = new Artist(id);
97                         artists.add(artist);
98                     }
99                 } finally {
100                     cursor.close();
101                 }
102             }
103         }
104 
105         // #2 Load albums and connect each to artist
106         {
107             Uri contentUri = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI;
108             String[] projection = {
109                 MediaStore.Audio.Albums._ID,
110                 MediaStore.Audio.Media.ARTIST_ID // TODO MediaStore.Audio.Albums.ARTIST_ID
111             };
112             String sortOrder = MediaStore.Audio.Albums._ID;
113             Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder);
114             if (cursor != null) {
115                 try {
116                     int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums._ID);
117                     int artistIdColumn = cursor.getColumnIndexOrThrow(
118                             MediaStore.Audio.Media.ARTIST_ID); // TODO MediaStore.Audio.Albums.ARTIST_ID
119 
120                     for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
121                         long id = cursor.getLong(idColumn);
122 
123                         Album album = new Album(id);
124                         albums.add(album);
125 
126                         if (!cursor.isNull(artistIdColumn)) {
127                             long artistId = cursor.getLong(artistIdColumn);
128 
129                             Artist artist = Collections.find(artists, artistId, Artist::getId);
130                             album.setArtist(artist);
131                         }
132                     }
133                 } finally {
134                     cursor.close();
135                 }
136             }
137         }
138 
139         // #3 Load songs and connect each to album and artist
140         {
141             Uri contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
142             String[] projection = {
143                 MediaStore.Audio.Media._ID,
144                 MediaStore.Audio.Media.MIME_TYPE,
145                 MediaStore.Audio.Media.ARTIST_ID,
146                 MediaStore.Audio.Media.ALBUM_ID
147             };
148             String selection = MediaStore.Audio.Media.IS_MUSIC + " != 0";
149             String sortOrder = MediaStore.Audio.Media._ID;
150             Cursor cursor = mContentResolver.query(contentUri, projection, selection, null, sortOrder);
151             if (cursor != null) {
152                 try {
153                     int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
154                     int mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.MIME_TYPE);
155                     int artistIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID);
156                     int albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID);
157 
158                     for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
159                         long id = cursor.getLong(idColumn);
160                         String mimeType = cursor.getString(mimeTypeColumn);
161 
162                         Audio audio = new Audio(id, mimeType);
163                         audios.add(audio);
164 
165                         if (!cursor.isNull(artistIdColumn)) {
166                             long artistId = cursor.getLong(artistIdColumn);
167 
168                             Artist artist = Collections.find(artists, artistId, Artist::getId);
169                             audio.setArtist(artist);
170                             artist.addAudio(audio);
171                         }
172                         if (!cursor.isNull(albumIdColumn)) {
173                             long albumId = cursor.getLong(albumIdColumn);
174 
175                             Album album = Collections.find(albums, albumId, Album::getId);
176                             audio.setAlbum(album);
177                             album.addAudio(audio);
178                         }
179                     }
180                 } finally {
181                     cursor.close();
182                 }
183             }
184         }
185 
186         // #4 Load playlists (optional?)
187         {
188             Uri contentUri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI;
189             String[] projection = {
190                 MediaStore.Audio.Playlists._ID
191             };
192             String sortOrder = MediaStore.Audio.Playlists._ID;
193             Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder);
194             if (cursor != null) {
195                 try {
196                     int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists._ID);
197 
198                     for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
199                         long id = cursor.getLong(idColumn);
200 
201                         Playlist playlist = new Playlist(id);
202                         playlists.add(playlist);
203                     }
204                 } finally {
205                     cursor.close();
206                 }
207             }
208         }
209 
210         // #5 Load genres (optional?)
211         {
212             Uri contentUri = MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI;
213             String[] projection = {
214                 MediaStore.Audio.Genres._ID
215             };
216             String sortOrder = MediaStore.Audio.Genres._ID;
217             Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder);
218             if (cursor != null) {
219                 try {
220                     int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID);
221 
222                     for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
223                         long id = cursor.getLong(idColumn);
224 
225                         Genre genre = new Genre(id);
226                         genres.add(genre);
227                     }
228                 } finally {
229                     cursor.close();
230                 }
231             }
232         }
233 
234         mChangeListener.onAudiosAdded(audios);
235         mChangeListener.onArtistsAdded(artists);
236         mChangeListener.onAlbumsAdded(albums);
237         mChangeListener.onGenresAdded(genres);
238         mChangeListener.onPlaylistsAdded(playlists);
239     }
240 
loadData(@onNull Audio audio)241     boolean loadData(@NonNull Audio audio) {
242         boolean updated = false;
243 
244         Uri contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
245         String[] projection = {
246             MediaStore.Audio.Media.TITLE,
247             MediaStore.Audio.Media.ARTIST_ID,
248             MediaStore.Audio.Media.ALBUM_ID
249         };
250         String selection = MediaStore.Audio.Media._ID + " = ?";
251         String[] selectionArgs = { Long.toString(audio.getId()) };
252         Cursor cursor = mContentResolver.query(
253                 contentUri, projection, selection, selectionArgs, null);
254         if (cursor != null) {
255             try {
256                 int titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE);
257                 int artistIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID);
258                 int albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID);
259 
260                 if (cursor.moveToFirst()) {
261                     if (!cursor.isNull(titleColumn)) {
262                         String title = cursor.getString(titleColumn);
263                         updated |= audio.setTitle(title);
264                     }
265                     if (!cursor.isNull(artistIdColumn)) {
266                         long artistId = cursor.getLong(artistIdColumn);
267                         Artist artist = mMediaProvider.getArtistById(artistId);
268                         updated |= audio.setArtist(artist);
269                         updated |= loadData(artist); // TODO(b/123707561) Load separate from audio
270                     }
271                     if (!cursor.isNull(albumIdColumn)) {
272                         long albumId = cursor.getLong(albumIdColumn);
273                         Album album = mMediaProvider.getAlbumById(albumId);
274                         updated |= audio.setAlbum(album);
275                         updated |= loadData(album); // TODO(b/123707561) Load separate from audio
276                     }
277                 }
278             } finally {
279                 cursor.close();
280             }
281         }
282 
283         return updated;
284     }
285 
loadData(@onNull Artist artist)286     boolean loadData(@NonNull Artist artist) {
287         boolean updated = false;
288 
289         Uri contentUri = MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI;
290         String[] projection = { MediaStore.Audio.Artists.ARTIST };
291         String selection = MediaStore.Audio.Artists._ID + " = ?";
292         String[] selectionArgs = { Long.toString(artist.getId()) };
293         Cursor cursor = mContentResolver.query(
294                 contentUri, projection, selection, selectionArgs, null);
295         if (cursor != null) {
296             try {
297                 int artistColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST);
298 
299                 if (cursor.moveToFirst()) {
300                     if (!cursor.isNull(artistColumn)) {
301                         String name = cursor.getString(artistColumn);
302                         updated |= artist.setName(name);
303                     }
304                 }
305             } finally {
306                 cursor.close();
307             }
308         }
309 
310         updated |= loadAlbums(artist); // TODO(b/123707561) Load separate from artist
311 
312         return updated;
313     }
314 
loadData(@onNull Album album)315     boolean loadData(@NonNull Album album) {
316         boolean updated = false;
317 
318         Uri contentUri = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI;
319         String[] projection = {
320             MediaStore.Audio.Albums.ALBUM_ART,
321             MediaStore.Audio.Albums.ALBUM,
322             MediaStore.Audio.Media.ARTIST_ID // TODO MediaStore.Audio.Albums.ARTIST_ID
323         };
324         String selection = MediaStore.Audio.Albums._ID + " = ?";
325         String[] selectionArgs = { Long.toString(album.getId()) };
326         Cursor cursor = mContentResolver.query(
327                 contentUri, projection, selection, selectionArgs, null);
328         if (cursor != null) {
329             try {
330                 int albumArtColumn = cursor.getColumnIndexOrThrow(
331                         MediaStore.Audio.Albums.ALBUM_ART);
332                 int albumColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM);
333                 int artistIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID); // TODO MediaStore.Audio.Albums.ARTIST_ID
334 
335                 if (cursor.moveToFirst()) {
336                     if (!cursor.isNull(albumColumn)) {
337                         String albumTitle = cursor.getString(albumColumn);
338                         updated |= album.setTitle(albumTitle);
339                     }
340                     if (!cursor.isNull(albumArtColumn)) {
341                         Uri albumArtUri = Uri.fromFile(new File(cursor.getString(albumArtColumn)));
342                         updated |= album.setAlbumArtUri(albumArtUri);
343                     }
344                     if (!cursor.isNull(artistIdColumn)) {
345                         long artistId = cursor.getLong(artistIdColumn);
346                         Artist artist = mMediaProvider.getArtistById(artistId);
347                         updated |= album.setArtist(artist);
348                         updated |= loadData(artist); // TODO(b/123707561) Load separate from album
349                     }
350                 }
351             } finally {
352                 cursor.close();
353             }
354         }
355 
356         return updated;
357     }
358 
loadData(@onNull Genre genre)359     boolean loadData(@NonNull Genre genre) {
360         boolean updated = false;
361 
362         Uri contentUri = MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI;
363         String[] projection = { MediaStore.Audio.Genres.NAME };
364         String selection = MediaStore.Audio.Genres._ID + " = ?";
365         String[] selectionArgs = { Long.toString(genre.getId()) };
366         Cursor cursor = mContentResolver.query(
367                 contentUri, projection, selection, selectionArgs, null);
368         if (cursor != null) {
369             try {
370                 int nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME);
371 
372                 if (cursor.moveToFirst()) {
373                     if (!cursor.isNull(nameColumn)) {
374                         String name = cursor.getString(nameColumn);
375                         updated |= genre.setName(name);
376                     }
377                 }
378             } finally {
379                 cursor.close();
380             }
381         }
382 
383         updated |= loadAudios(genre); // TODO(b/123707561) Load separate from genre
384 
385         return updated;
386     }
387 
loadData(@onNull Playlist playlist)388     boolean loadData(@NonNull Playlist playlist) {
389         boolean updated = false;
390 
391         Uri contentUri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI;
392         String[] projection = { MediaStore.Audio.Playlists.NAME };
393         String selection = MediaStore.Audio.Playlists._ID + " = ?";
394         String[] selectionArgs = { Long.toString(playlist.getId()) };
395         Cursor cursor = mContentResolver.query(
396                 contentUri, projection, selection, selectionArgs, null);
397         if (cursor != null) {
398             try {
399                 int nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.NAME);
400 
401                 if (cursor.moveToFirst()) {
402                     if (!cursor.isNull(nameColumn)) {
403                         String name = cursor.getString(nameColumn);
404                         updated |= playlist.setName(name);
405                     }
406                 }
407             } finally {
408                 cursor.close();
409             }
410         }
411 
412         updated |= loadAudios(playlist); // TODO(b/123707561) Load separate from playlist
413 
414         return updated;
415     }
416 
loadAlbums(@onNull Artist artist)417     boolean loadAlbums(@NonNull Artist artist) {
418         boolean updated = false;
419 
420         // TODO Remove hardcoded value
421         Uri contentUri = MediaStore.Audio.Artists.Albums.getContentUri("external", artist.getId());
422         /*
423          * On some devices MediaStore doesn't use ALBUM_ID as key from Artist to Album, but rather
424          * _ID. In order to support these devices we don't pass a projection, to avoid the
425          * IllegalArgumentException(Invalid column) exception, and then resort to _ID.
426          */
427         String[] projection = null; // { MediaStore.Audio.Artists.Albums.ALBUM_ID };
428         Cursor cursor = mContentResolver.query(contentUri, projection, null, null, null);
429         if (cursor != null) {
430             try {
431                 int albumIdColumn = cursor.getColumnIndex(MediaStore.Audio.Artists.Albums.ALBUM_ID);
432                 if (albumIdColumn < 0) {
433                     // On some devices the ALBUM_ID column doesn't exist and _ID is used instead.
434                     albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
435                 }
436 
437                 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
438                     long albumId = cursor.getLong(albumIdColumn);
439                     Album album = mMediaProvider.getAlbumById(albumId);
440                     updated |= artist.addAlbum(album);
441                     //updated |= loadData(album); // TODO(b/123707561) Load separate from artist
442                 }
443             } finally {
444                 cursor.close();
445             }
446         }
447 
448         return updated;
449     }
450 
loadAudios(@onNull Genre genre)451     boolean loadAudios(@NonNull Genre genre) {
452         boolean updated = false;
453 
454         // TODO Remove hardcoded value
455         Uri contentUri = MediaStore.Audio.Genres.Members.getContentUri("external", genre.getId());
456         String[] projection = { MediaStore.Audio.Genres.Members.AUDIO_ID };
457         Cursor cursor = mContentResolver.query(contentUri, projection, null, null, null);
458         if (cursor != null) {
459             try {
460                 int audioIdColumn = cursor.getColumnIndexOrThrow(
461                         MediaStore.Audio.Genres.Members.AUDIO_ID);
462 
463                 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
464                     long audioId = cursor.getLong(audioIdColumn);
465                     Audio audio = mMediaProvider.getAudioById(audioId);
466                     updated |= genre.addAudio(audio);
467                     updated |= loadData(audio); // TODO(b/123707561) Load separate from genre
468                 }
469             } finally {
470                 cursor.close();
471             }
472         }
473 
474         return updated;
475     }
476 
loadAudios(@onNull Playlist playlist)477     boolean loadAudios(@NonNull Playlist playlist) {
478         boolean updated = false;
479 
480         // TODO Remove hardcoded value
481         Uri contentUri = MediaStore.Audio.Playlists.Members.getContentUri(
482                 "external", playlist.getId());
483         String[] projection = { MediaStore.Audio.Playlists.Members.AUDIO_ID };
484         Cursor cursor = mContentResolver.query(contentUri, projection, null, null, null);
485         if (cursor != null) {
486             try {
487                 int audioIdColumn = cursor.getColumnIndexOrThrow(
488                         MediaStore.Audio.Playlists.Members.AUDIO_ID);
489 
490                 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
491                     long audioId = cursor.getLong(audioIdColumn);
492                     Audio audio = mMediaProvider.getAudioById(audioId);
493                     updated |= playlist.addAudio(audio);
494                     updated |= loadData(audio); // TODO(b/123707561) Load separate from playlist
495                 }
496             } finally {
497                 cursor.close();
498             }
499         }
500 
501         return updated;
502     }
503 
504     @Override
onChange(boolean selfChange)505     public void onChange(boolean selfChange) {
506         Clog.i(TAG, "onChange(" + selfChange + ")");
507         onChange(selfChange, null);
508     }
509 
510     @Override
onChange(boolean selfChange, @Nullable Uri uri)511     public void onChange(boolean selfChange, @Nullable Uri uri) {
512         Clog.i(TAG, "onChange(" + selfChange + ", " + uri + ")");
513         // TODO(123705758) Figure out what changed
514         // onChange(false, content://media)
515         // onChange(false, content://media/external)
516         // onChange(false, content://media/external/audio/media/444)
517         // onChange(false, content://media/external/video/media/328?blocking=1&orig_id=328&group_id=0)
518 
519         // TODO(123705758) Notify listener about changes
520         // mChangeListener.xxx();
521     }
522 
523     // TODO Remove unused methods
createPlaylist(@onNull String name)524     private long createPlaylist(@NonNull String name) {
525         Clog.i(TAG, "createPlaylist(" + name + ")");
526         Uri contentUri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI;
527         ContentValues contentValues = new ContentValues(1);
528         contentValues.put(MediaStore.Audio.Playlists.NAME, name);
529         Uri uri = mContentResolver.insert(contentUri, contentValues);
530         return Long.parseLong(uri.getLastPathSegment());
531     }
532 
addToPlaylist(@onNull Playlist playlist, @NonNull Audio audio)533     private void addToPlaylist(@NonNull Playlist playlist, @NonNull Audio audio) {
534         Clog.i(TAG, "addToPlaylist(" + playlist + ", " + audio + ")");
535         long base = getLastPlayOrder(playlist);
536 
537         // TODO Remove hardcoded value
538         Uri contentUri = MediaStore.Audio.Playlists.Members.getContentUri(
539                 "external", playlist.getId());
540         ContentValues contentValues = new ContentValues(2);
541         contentValues.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audio.getId());
542         contentValues.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, base + 1);
543         mContentResolver.insert(contentUri, contentValues);
544     }
545 
getLastPlayOrder(@onNull Playlist playlist)546     private long getLastPlayOrder(@NonNull Playlist playlist) {
547         Clog.i(TAG, "getLastPlayOrder(" + playlist + ")");
548 
549         long playOrder = -1;
550 
551         // TODO Remove hardcoded value
552         Uri contentUri = MediaStore.Audio.Playlists.Members.getContentUri(
553                 "external", playlist.getId());
554         String[] projection = { MediaStore.Audio.Playlists.Members.PLAY_ORDER };
555         String sortOrder = MediaStore.Audio.Playlists.Members.PLAY_ORDER + " DESC LIMIT 1";
556         Cursor cursor = mContentResolver.query(
557                 contentUri, projection, null, null, sortOrder);
558         if (cursor != null) {
559             try {
560                 int playOrderColumn = cursor.getColumnIndexOrThrow(
561                         MediaStore.Audio.Playlists.Members.PLAY_ORDER);
562 
563                 if (cursor.moveToFirst()) {
564                     playOrder = cursor.getLong(playOrderColumn);
565                 }
566             } finally {
567                 cursor.close();
568             }
569         }
570 
571         return playOrder;
572     }
573 }
574