1/*
2 * Copyright (C) 2008 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
17package com.android.music;
18
19import android.app.Activity;
20import android.content.ComponentName;
21import android.content.ContentResolver;
22import android.content.ContentUris;
23import android.content.ContentValues;
24import android.content.Context;
25import android.content.ContextWrapper;
26import android.content.Intent;
27import android.content.ServiceConnection;
28import android.content.SharedPreferences;
29import android.content.SharedPreferences.Editor;
30import android.content.res.Resources;
31import android.database.Cursor;
32import android.graphics.Bitmap;
33import android.graphics.BitmapFactory;
34import android.graphics.Canvas;
35import android.graphics.ColorFilter;
36import android.graphics.ColorMatrix;
37import android.graphics.ColorMatrixColorFilter;
38import android.graphics.Matrix;
39import android.graphics.Paint;
40import android.graphics.PixelFormat;
41import android.graphics.drawable.BitmapDrawable;
42import android.graphics.drawable.Drawable;
43import android.net.Uri;
44import android.os.Environment;
45import android.os.ParcelFileDescriptor;
46import android.os.RemoteException;
47import android.provider.MediaStore;
48import android.provider.Settings;
49import android.text.TextUtils;
50import android.text.format.Time;
51import android.util.Log;
52import android.view.Menu;
53import android.view.MenuItem;
54import android.view.SubMenu;
55import android.view.View;
56import android.view.Window;
57import android.widget.TabWidget;
58import android.widget.TextView;
59import android.widget.Toast;
60
61import java.io.File;
62import java.io.FileDescriptor;
63import java.io.FileNotFoundException;
64import java.io.IOException;
65import java.io.InputStream;
66import java.io.PrintWriter;
67import java.util.Arrays;
68import java.util.Formatter;
69import java.util.HashMap;
70import java.util.Locale;
71
72public class MusicUtils {
73    private static final String TAG = "MusicUtils";
74
75    public interface Defs {
76        public final static int OPEN_URL = 0;
77        public final static int ADD_TO_PLAYLIST = 1;
78        public final static int USE_AS_RINGTONE = 2;
79        public final static int PLAYLIST_SELECTED = 3;
80        public final static int NEW_PLAYLIST = 4;
81        public final static int PLAY_SELECTION = 5;
82        public final static int GOTO_START = 6;
83        public final static int GOTO_PLAYBACK = 7;
84        public final static int PARTY_SHUFFLE = 8;
85        public final static int SHUFFLE_ALL = 9;
86        public final static int DELETE_ITEM = 10;
87        public final static int SCAN_DONE = 11;
88        public final static int QUEUE = 12;
89        public final static int EFFECTS_PANEL = 13;
90        public final static int CHILD_MENU_BASE = 14; // this should be the last item
91    }
92
93    public static String makeAlbumsLabel(
94            Context context, int numalbums, int numsongs, boolean isUnknown) {
95        // There are two formats for the albums/songs information:
96        // "N Song(s)"  - used for unknown artist/album
97        // "N Album(s)" - used for known albums
98
99        StringBuilder songs_albums = new StringBuilder();
100
101        Resources r = context.getResources();
102        if (isUnknown) {
103            if (numsongs == 1) {
104                songs_albums.append(context.getString(R.string.onesong));
105            } else {
106                String f = r.getQuantityText(R.plurals.Nsongs, numsongs).toString();
107                sFormatBuilder.setLength(0);
108                sFormatter.format(f, Integer.valueOf(numsongs));
109                songs_albums.append(sFormatBuilder);
110            }
111        } else {
112            String f = r.getQuantityText(R.plurals.Nalbums, numalbums).toString();
113            sFormatBuilder.setLength(0);
114            sFormatter.format(f, Integer.valueOf(numalbums));
115            songs_albums.append(sFormatBuilder);
116            songs_albums.append(context.getString(R.string.albumsongseparator));
117        }
118        return songs_albums.toString();
119    }
120
121    /**
122     * This is now only used for the query screen
123     */
124    public static String makeAlbumsSongsLabel(
125            Context context, int numalbums, int numsongs, boolean isUnknown) {
126        // There are several formats for the albums/songs information:
127        // "1 Song"   - used if there is only 1 song
128        // "N Songs" - used for the "unknown artist" item
129        // "1 Album"/"N Songs"
130        // "N Album"/"M Songs"
131        // Depending on locale, these may need to be further subdivided
132
133        StringBuilder songs_albums = new StringBuilder();
134
135        if (numsongs == 1) {
136            songs_albums.append(context.getString(R.string.onesong));
137        } else {
138            Resources r = context.getResources();
139            if (!isUnknown) {
140                String f = r.getQuantityText(R.plurals.Nalbums, numalbums).toString();
141                sFormatBuilder.setLength(0);
142                sFormatter.format(f, Integer.valueOf(numalbums));
143                songs_albums.append(sFormatBuilder);
144                songs_albums.append(context.getString(R.string.albumsongseparator));
145            }
146            String f = r.getQuantityText(R.plurals.Nsongs, numsongs).toString();
147            sFormatBuilder.setLength(0);
148            sFormatter.format(f, Integer.valueOf(numsongs));
149            songs_albums.append(sFormatBuilder);
150        }
151        return songs_albums.toString();
152    }
153
154    public static IMediaPlaybackService sService = null;
155    private static HashMap<Context, ServiceBinder> sConnectionMap =
156            new HashMap<Context, ServiceBinder>();
157
158    public static class ServiceToken {
159        ContextWrapper mWrappedContext;
160        ServiceToken(ContextWrapper context) {
161            mWrappedContext = context;
162        }
163    }
164
165    public static ServiceToken bindToService(Activity context) {
166        return bindToService(context, null);
167    }
168
169    public static ServiceToken bindToService(Activity context, ServiceConnection callback) {
170        Activity realActivity = context.getParent();
171        if (realActivity == null) {
172            realActivity = context;
173        }
174        ContextWrapper cw = new ContextWrapper(realActivity);
175        cw.startService(new Intent(cw, MediaPlaybackService.class));
176        ServiceBinder sb = new ServiceBinder(callback);
177        if (cw.bindService((new Intent()).setClass(cw, MediaPlaybackService.class), sb, 0)) {
178            sConnectionMap.put(cw, sb);
179            return new ServiceToken(cw);
180        }
181        Log.e("Music", "Failed to bind to service");
182        return null;
183    }
184
185    public static void unbindFromService(ServiceToken token) {
186        if (token == null) {
187            Log.e("MusicUtils", "Trying to unbind with null token");
188            return;
189        }
190        ContextWrapper cw = token.mWrappedContext;
191        ServiceBinder sb = sConnectionMap.remove(cw);
192        if (sb == null) {
193            Log.e("MusicUtils", "Trying to unbind for unknown Context");
194            return;
195        }
196        cw.unbindService(sb);
197        if (sConnectionMap.isEmpty()) {
198            // presumably there is nobody interested in the service at this point,
199            // so don't hang on to the ServiceConnection
200            sService = null;
201        }
202    }
203
204    private static class ServiceBinder implements ServiceConnection {
205        ServiceConnection mCallback;
206        ServiceBinder(ServiceConnection callback) {
207            mCallback = callback;
208        }
209
210        public void onServiceConnected(ComponentName className, android.os.IBinder service) {
211            sService = IMediaPlaybackService.Stub.asInterface(service);
212            initAlbumArtCache();
213            if (mCallback != null) {
214                mCallback.onServiceConnected(className, service);
215            }
216        }
217
218        public void onServiceDisconnected(ComponentName className) {
219            if (mCallback != null) {
220                mCallback.onServiceDisconnected(className);
221            }
222            sService = null;
223        }
224    }
225
226    public static long getCurrentAlbumId() {
227        if (sService != null) {
228            try {
229                return sService.getAlbumId();
230            } catch (RemoteException ex) {
231            }
232        }
233        return -1;
234    }
235
236    public static long getCurrentArtistId() {
237        if (MusicUtils.sService != null) {
238            try {
239                return sService.getArtistId();
240            } catch (RemoteException ex) {
241            }
242        }
243        return -1;
244    }
245
246    public static long getCurrentAudioId() {
247        if (MusicUtils.sService != null) {
248            try {
249                return sService.getAudioId();
250            } catch (RemoteException ex) {
251            }
252        }
253        return -1;
254    }
255
256    public static int getCurrentShuffleMode() {
257        int mode = MediaPlaybackService.SHUFFLE_NONE;
258        if (sService != null) {
259            try {
260                mode = sService.getShuffleMode();
261            } catch (RemoteException ex) {
262            }
263        }
264        return mode;
265    }
266
267    public static void togglePartyShuffle() {
268        if (sService != null) {
269            int shuffle = getCurrentShuffleMode();
270            try {
271                if (shuffle == MediaPlaybackService.SHUFFLE_AUTO) {
272                    sService.setShuffleMode(MediaPlaybackService.SHUFFLE_NONE);
273                } else {
274                    sService.setShuffleMode(MediaPlaybackService.SHUFFLE_AUTO);
275                }
276            } catch (RemoteException ex) {
277            }
278        }
279    }
280
281    public static void setPartyShuffleMenuIcon(Menu menu) {
282        MenuItem item = menu.findItem(Defs.PARTY_SHUFFLE);
283        if (item != null) {
284            int shuffle = MusicUtils.getCurrentShuffleMode();
285            if (shuffle == MediaPlaybackService.SHUFFLE_AUTO) {
286                item.setIcon(R.drawable.ic_menu_party_shuffle);
287                item.setTitle(R.string.party_shuffle_off);
288            } else {
289                item.setIcon(R.drawable.ic_menu_party_shuffle);
290                item.setTitle(R.string.party_shuffle);
291            }
292        }
293    }
294
295    /*
296     * Returns true if a file is currently opened for playback (regardless
297     * of whether it's playing or paused).
298     */
299    public static boolean isMusicLoaded() {
300        if (MusicUtils.sService != null) {
301            try {
302                return sService.getPath() != null;
303            } catch (RemoteException ex) {
304            }
305        }
306        return false;
307    }
308
309    private final static long[] sEmptyList = new long[0];
310
311    public static long[] getSongListForCursor(Cursor cursor) {
312        if (cursor == null) {
313            return sEmptyList;
314        }
315        int len = cursor.getCount();
316        long[] list = new long[len];
317        cursor.moveToFirst();
318        int colidx = -1;
319        try {
320            colidx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
321        } catch (IllegalArgumentException ex) {
322            colidx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
323        }
324        for (int i = 0; i < len; i++) {
325            list[i] = cursor.getLong(colidx);
326            cursor.moveToNext();
327        }
328        return list;
329    }
330
331    public static long[] getSongListForArtist(Context context, long id) {
332        final String[] ccols = new String[] {MediaStore.Audio.Media._ID};
333        String where = MediaStore.Audio.Media.ARTIST_ID + "=" + id + " AND "
334                + MediaStore.Audio.Media.IS_MUSIC + "=1";
335        Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, ccols, where,
336                null, MediaStore.Audio.Media.ALBUM_KEY + "," + MediaStore.Audio.Media.TRACK);
337
338        if (cursor != null) {
339            long[] list = getSongListForCursor(cursor);
340            cursor.close();
341            return list;
342        }
343        return sEmptyList;
344    }
345
346    public static long[] getSongListForAlbum(Context context, long id) {
347        final String[] ccols = new String[] {MediaStore.Audio.Media._ID};
348        String where = MediaStore.Audio.Media.ALBUM_ID + "=" + id + " AND "
349                + MediaStore.Audio.Media.IS_MUSIC + "=1";
350        Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, ccols, where,
351                null, MediaStore.Audio.Media.TRACK);
352
353        if (cursor != null) {
354            long[] list = getSongListForCursor(cursor);
355            cursor.close();
356            return list;
357        }
358        return sEmptyList;
359    }
360
361    public static long[] getSongListForPlaylist(Context context, long plid) {
362        final String[] ccols = new String[] {MediaStore.Audio.Playlists.Members.AUDIO_ID};
363        Cursor cursor =
364                query(context, MediaStore.Audio.Playlists.Members.getContentUri("external", plid),
365                        ccols, null, null, MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);
366
367        if (cursor != null) {
368            long[] list = getSongListForCursor(cursor);
369            cursor.close();
370            return list;
371        }
372        return sEmptyList;
373    }
374
375    public static void playPlaylist(Context context, long plid) {
376        long[] list = getSongListForPlaylist(context, plid);
377        if (list != null) {
378            playAll(context, list, -1, false);
379        }
380    }
381
382    public static long[] getAllSongs(Context context) {
383        Cursor c = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
384                new String[] {MediaStore.Audio.Media._ID}, MediaStore.Audio.Media.IS_MUSIC + "=1",
385                null, null);
386        try {
387            if (c == null || c.getCount() == 0) {
388                return null;
389            }
390            int len = c.getCount();
391            long[] list = new long[len];
392            for (int i = 0; i < len; i++) {
393                c.moveToNext();
394                list[i] = c.getLong(0);
395            }
396
397            return list;
398        } finally {
399            if (c != null) {
400                c.close();
401            }
402        }
403    }
404
405    /**
406     * Fills out the given submenu with items for "new playlist" and
407     * any existing playlists. When the user selects an item, the
408     * application will receive PLAYLIST_SELECTED with the Uri of
409     * the selected playlist, NEW_PLAYLIST if a new playlist
410     * should be created, and QUEUE if the "current playlist" was
411     * selected.
412     * @param context The context to use for creating the menu items
413     * @param sub The submenu to add the items to.
414     */
415    public static void makePlaylistMenu(Context context, SubMenu sub) {
416        String[] cols =
417                new String[] {MediaStore.Audio.Playlists._ID, MediaStore.Audio.Playlists.NAME};
418        ContentResolver resolver = context.getContentResolver();
419        if (resolver == null) {
420            System.out.println("resolver = null");
421        } else {
422            String whereclause = MediaStore.Audio.Playlists.NAME + " != ''";
423            Cursor cur = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, cols,
424                    whereclause, null, MediaStore.Audio.Playlists.NAME);
425            sub.clear();
426            sub.add(1, Defs.QUEUE, 0, R.string.queue);
427            sub.add(1, Defs.NEW_PLAYLIST, 0, R.string.new_playlist);
428            if (cur != null && cur.getCount() > 0) {
429                // sub.addSeparator(1, 0);
430                cur.moveToFirst();
431                while (!cur.isAfterLast()) {
432                    Intent intent = new Intent();
433                    intent.putExtra("playlist", cur.getLong(0));
434                    //                    if (cur.getInt(0) == mLastPlaylistSelected) {
435                    //                        sub.add(0, MusicBaseActivity.PLAYLIST_SELECTED,
436                    //                        cur.getString(1)).setIntent(intent);
437                    //                    } else {
438                    sub.add(1, Defs.PLAYLIST_SELECTED, 0, cur.getString(1)).setIntent(intent);
439                    //                    }
440                    cur.moveToNext();
441                }
442            }
443            if (cur != null) {
444                cur.close();
445            }
446        }
447    }
448
449    public static void clearPlaylist(Context context, int plid) {
450        Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", plid);
451        context.getContentResolver().delete(uri, null, null);
452        return;
453    }
454
455    public static void deleteTracks(Context context, long[] list) {
456        String[] cols = new String[] {MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DATA,
457                MediaStore.Audio.Media.ALBUM_ID};
458        StringBuilder where = new StringBuilder();
459        where.append(MediaStore.Audio.Media._ID + " IN (");
460        for (int i = 0; i < list.length; i++) {
461            where.append(list[i]);
462            if (i < list.length - 1) {
463                where.append(",");
464            }
465        }
466        where.append(")");
467        Cursor c = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, cols,
468                where.toString(), null, null);
469
470        if (c != null) {
471            // step 1: remove selected tracks from the current playlist, as well
472            // as from the album art cache
473            try {
474                c.moveToFirst();
475                while (!c.isAfterLast()) {
476                    // remove from current playlist
477                    long id = c.getLong(0);
478                    sService.removeTrack(id);
479                    // remove from album art cache
480                    long artIndex = c.getLong(2);
481                    synchronized (sArtCache) {
482                        sArtCache.remove(artIndex);
483                    }
484                    c.moveToNext();
485                }
486            } catch (RemoteException ex) {
487            }
488
489            // step 2: remove selected tracks from the database
490            context.getContentResolver().delete(
491                    MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, where.toString(), null);
492
493            // step 3: remove files from card
494            c.moveToFirst();
495            while (!c.isAfterLast()) {
496                String name = c.getString(1);
497                File f = new File(name);
498                try { // File.delete can throw a security exception
499                    if (!f.delete()) {
500                        // I'm not sure if we'd ever get here (deletion would
501                        // have to fail, but no exception thrown)
502                        Log.e("MusicUtils", "Failed to delete file " + name);
503                    }
504                    c.moveToNext();
505                } catch (SecurityException ex) {
506                    c.moveToNext();
507                }
508            }
509            c.close();
510        }
511
512        String message = context.getResources().getQuantityString(
513                R.plurals.NNNtracksdeleted, list.length, Integer.valueOf(list.length));
514
515        Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
516        // We deleted a number of tracks, which could affect any number of things
517        // in the media content domain, so update everything.
518        context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
519    }
520
521    public static void addToCurrentPlaylist(Context context, long[] list) {
522        if (sService == null) {
523            return;
524        }
525        try {
526            sService.enqueue(list, MediaPlaybackService.LAST);
527            String message = context.getResources().getQuantityString(
528                    R.plurals.NNNtrackstoplaylist, list.length, Integer.valueOf(list.length));
529            Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
530        } catch (RemoteException ex) {
531        }
532    }
533
534    private static ContentValues[] sContentValuesCache = null;
535
536    /**
537     * @param ids The source array containing all the ids to be added to the playlist
538     * @param offset Where in the 'ids' array we start reading
539     * @param len How many items to copy during this pass
540     * @param base The play order offset to use for this pass
541     */
542    private static void makeInsertItems(long[] ids, int offset, int len, int base) {
543        // adjust 'len' if would extend beyond the end of the source array
544        if (offset + len > ids.length) {
545            len = ids.length - offset;
546        }
547        // allocate the ContentValues array, or reallocate if it is the wrong size
548        if (sContentValuesCache == null || sContentValuesCache.length != len) {
549            sContentValuesCache = new ContentValues[len];
550        }
551        // fill in the ContentValues array with the right values for this pass
552        for (int i = 0; i < len; i++) {
553            if (sContentValuesCache[i] == null) {
554                sContentValuesCache[i] = new ContentValues();
555            }
556
557            sContentValuesCache[i].put(
558                    MediaStore.Audio.Playlists.Members.PLAY_ORDER, base + offset + i);
559            sContentValuesCache[i].put(
560                    MediaStore.Audio.Playlists.Members.AUDIO_ID, ids[offset + i]);
561        }
562    }
563
564    public static void addToPlaylist(Context context, long[] ids, long playlistid) {
565        if (ids == null) {
566            // this shouldn't happen (the menuitems shouldn't be visible
567            // unless the selected item represents something playable
568            Log.e("MusicBase", "ListSelection null");
569        } else {
570            int size = ids.length;
571            ContentResolver resolver = context.getContentResolver();
572            // need to determine the number of items currently in the playlist,
573            // so the play_order field can be maintained.
574            String[] cols = new String[] {"count(*)"};
575            Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid);
576            Cursor cur = resolver.query(uri, cols, null, null, null);
577            cur.moveToFirst();
578            int base = cur.getInt(0);
579            cur.close();
580            int numinserted = 0;
581            for (int i = 0; i < size; i += 1000) {
582                makeInsertItems(ids, i, 1000, base);
583                numinserted += resolver.bulkInsert(uri, sContentValuesCache);
584            }
585            String message = context.getResources().getQuantityString(
586                    R.plurals.NNNtrackstoplaylist, numinserted, numinserted);
587            Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
588            // mLastPlaylistSelected = playlistid;
589        }
590    }
591
592    public static Cursor query(Context context, Uri uri, String[] projection, String selection,
593            String[] selectionArgs, String sortOrder, int limit) {
594        try {
595            ContentResolver resolver = context.getContentResolver();
596            if (resolver == null) {
597                return null;
598            }
599            if (limit > 0) {
600                uri = uri.buildUpon().appendQueryParameter("limit", "" + limit).build();
601            }
602            return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
603        } catch (UnsupportedOperationException ex) {
604            return null;
605        }
606    }
607    public static Cursor query(Context context, Uri uri, String[] projection, String selection,
608            String[] selectionArgs, String sortOrder) {
609        return query(context, uri, projection, selection, selectionArgs, sortOrder, 0);
610    }
611
612    public static boolean isMediaScannerScanning(Context context) {
613        boolean result = false;
614        Cursor cursor = query(context, MediaStore.getMediaScannerUri(),
615                new String[] {MediaStore.MEDIA_SCANNER_VOLUME}, null, null, null);
616        if (cursor != null) {
617            if (cursor.getCount() == 1) {
618                cursor.moveToFirst();
619                result = "external".equals(cursor.getString(0));
620            }
621            cursor.close();
622        }
623
624        return result;
625    }
626
627    public static void setSpinnerState(Activity a) {
628        if (isMediaScannerScanning(a)) {
629            // start the progress spinner
630            a.getWindow().setFeatureInt(
631                    Window.FEATURE_INDETERMINATE_PROGRESS, Window.PROGRESS_INDETERMINATE_ON);
632
633            a.getWindow().setFeatureInt(
634                    Window.FEATURE_INDETERMINATE_PROGRESS, Window.PROGRESS_VISIBILITY_ON);
635        } else {
636            // stop the progress spinner
637            a.getWindow().setFeatureInt(
638                    Window.FEATURE_INDETERMINATE_PROGRESS, Window.PROGRESS_VISIBILITY_OFF);
639        }
640    }
641
642    private static String mLastSdStatus;
643
644    public static void displayDatabaseError(Activity a) {
645        if (a.isFinishing()) {
646            // When switching tabs really fast, we can end up with a null
647            // cursor (not sure why), which will bring us here.
648            // Don't bother showing an error message in that case.
649            return;
650        }
651
652        String status = Environment.getExternalStorageState();
653        int title, message;
654
655        if (android.os.Environment.isExternalStorageRemovable()) {
656            title = R.string.sdcard_error_title;
657            message = R.string.sdcard_error_message;
658        } else {
659            title = R.string.sdcard_error_title_nosdcard;
660            message = R.string.sdcard_error_message_nosdcard;
661        }
662
663        if (status.equals(Environment.MEDIA_SHARED) || status.equals(Environment.MEDIA_UNMOUNTED)) {
664            if (android.os.Environment.isExternalStorageRemovable()) {
665                title = R.string.sdcard_busy_title;
666                message = R.string.sdcard_busy_message;
667            } else {
668                title = R.string.sdcard_busy_title_nosdcard;
669                message = R.string.sdcard_busy_message_nosdcard;
670            }
671        } else if (status.equals(Environment.MEDIA_REMOVED)) {
672            if (android.os.Environment.isExternalStorageRemovable()) {
673                title = R.string.sdcard_missing_title;
674                message = R.string.sdcard_missing_message;
675            } else {
676                title = R.string.sdcard_missing_title_nosdcard;
677                message = R.string.sdcard_missing_message_nosdcard;
678            }
679        } else if (status.equals(Environment.MEDIA_MOUNTED)) {
680            // The card is mounted, but we didn't get a valid cursor.
681            // This probably means the mediascanner hasn't started scanning the
682            // card yet (there is a small window of time during boot where this
683            // will happen).
684            a.setTitle("");
685            Intent intent = new Intent();
686            intent.setClass(a, ScanningProgress.class);
687            a.startActivityForResult(intent, Defs.SCAN_DONE);
688        } else if (!TextUtils.equals(mLastSdStatus, status)) {
689            mLastSdStatus = status;
690            Log.d(TAG, "sd card: " + status);
691        }
692
693        a.setTitle(title);
694        View v = a.findViewById(R.id.sd_message);
695        if (v != null) {
696            v.setVisibility(View.VISIBLE);
697        }
698        v = a.findViewById(R.id.sd_icon);
699        if (v != null) {
700            v.setVisibility(View.VISIBLE);
701        }
702        v = a.findViewById(android.R.id.list);
703        if (v != null) {
704            v.setVisibility(View.GONE);
705        }
706        v = a.findViewById(R.id.buttonbar);
707        if (v != null) {
708            v.setVisibility(View.GONE);
709        }
710        TextView tv = (TextView) a.findViewById(R.id.sd_message);
711        tv.setText(message);
712    }
713
714    public static void hideDatabaseError(Activity a) {
715        View v = a.findViewById(R.id.sd_message);
716        if (v != null) {
717            v.setVisibility(View.GONE);
718        }
719        v = a.findViewById(R.id.sd_icon);
720        if (v != null) {
721            v.setVisibility(View.GONE);
722        }
723        v = a.findViewById(android.R.id.list);
724        if (v != null) {
725            v.setVisibility(View.VISIBLE);
726        }
727    }
728
729    static protected Uri getContentURIForPath(String path) {
730        return Uri.fromFile(new File(path));
731    }
732
733    /*  Try to use String.format() as little as possible, because it creates a
734     *  new Formatter every time you call it, which is very inefficient.
735     *  Reusing an existing Formatter more than tripled the speed of
736     *  makeTimeString().
737     *  This Formatter/StringBuilder are also used by makeAlbumSongsLabel()
738     */
739    private static StringBuilder sFormatBuilder = new StringBuilder();
740    private static Formatter sFormatter = new Formatter(sFormatBuilder, Locale.getDefault());
741    private static final Object[] sTimeArgs = new Object[5];
742
743    public static String makeTimeString(Context context, long secs) {
744        String durationformat = context.getString(
745                secs < 3600 ? R.string.durationformatshort : R.string.durationformatlong);
746
747        /* Provide multiple arguments so the format can be changed easily
748         * by modifying the xml.
749         */
750        sFormatBuilder.setLength(0);
751
752        final Object[] timeArgs = sTimeArgs;
753        timeArgs[0] = secs / 3600;
754        timeArgs[1] = secs / 60;
755        timeArgs[2] = (secs / 60) % 60;
756        timeArgs[3] = secs;
757        timeArgs[4] = secs % 60;
758
759        return sFormatter.format(durationformat, timeArgs).toString();
760    }
761
762    public static void shuffleAll(Context context, Cursor cursor) {
763        playAll(context, cursor, 0, true);
764    }
765
766    public static void playAll(Context context, Cursor cursor) {
767        playAll(context, cursor, 0, false);
768    }
769
770    public static void playAll(Context context, Cursor cursor, int position) {
771        playAll(context, cursor, position, false);
772    }
773
774    public static void playAll(Context context, long[] list, int position) {
775        playAll(context, list, position, false);
776    }
777
778    private static void playAll(
779            Context context, Cursor cursor, int position, boolean force_shuffle) {
780        long[] list = getSongListForCursor(cursor);
781        playAll(context, list, position, force_shuffle);
782    }
783
784    private static void playAll(Context context, long[] list, int position, boolean force_shuffle) {
785        if (list.length == 0 || sService == null) {
786            Log.d("MusicUtils", "attempt to play empty song list");
787            // Don't try to play empty playlists. Nothing good will come of it.
788            String message = context.getString(R.string.emptyplaylist, list.length);
789            Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
790            return;
791        }
792        try {
793            if (force_shuffle) {
794                sService.setShuffleMode(MediaPlaybackService.SHUFFLE_NORMAL);
795            }
796            long curid = sService.getAudioId();
797            int curpos = sService.getQueuePosition();
798            if (position != -1 && curpos == position && curid == list[position]) {
799                // The selected file is the file that's currently playing;
800                // figure out if we need to restart with a new playlist,
801                // or just launch the playback activity.
802                long[] playlist = sService.getQueue();
803                if (Arrays.equals(list, playlist)) {
804                    // we don't need to set a new list, but we should resume playback if needed
805                    sService.play();
806                    return; // the 'finally' block will still run
807                }
808            }
809            if (position < 0) {
810                position = 0;
811            }
812            sService.open(list, force_shuffle ? -1 : position);
813            sService.play();
814        } catch (RemoteException ex) {
815        } finally {
816            Intent intent = new Intent("com.android.music.PLAYBACK_VIEWER")
817                                    .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
818            context.startActivity(intent);
819        }
820    }
821
822    public static void clearQueue() {
823        try {
824            sService.removeTracks(0, Integer.MAX_VALUE);
825        } catch (RemoteException ex) {
826        }
827    }
828
829    // A really simple BitmapDrawable-like class, that doesn't do
830    // scaling, dithering or filtering.
831    private static class FastBitmapDrawable extends Drawable {
832        private Bitmap mBitmap;
833        public FastBitmapDrawable(Bitmap b) {
834            mBitmap = b;
835        }
836        @Override
837        public void draw(Canvas canvas) {
838            canvas.drawBitmap(mBitmap, 0, 0, null);
839        }
840        @Override
841        public int getOpacity() {
842            return PixelFormat.OPAQUE;
843        }
844        @Override
845        public void setAlpha(int alpha) {}
846        @Override
847        public void setColorFilter(ColorFilter cf) {}
848    }
849
850    private static int sArtId = -2;
851    private static Bitmap mCachedBit = null;
852    private static final BitmapFactory.Options sBitmapOptionsCache = new BitmapFactory.Options();
853    private static final BitmapFactory.Options sBitmapOptions = new BitmapFactory.Options();
854    private static final Uri sArtworkUri = Uri.parse("content://media/external/audio/albumart");
855    private static final HashMap<Long, Drawable> sArtCache = new HashMap<Long, Drawable>();
856    private static int sArtCacheId = -1;
857
858    static {
859        // for the cache,
860        // 565 is faster to decode and display
861        // and we don't want to dither here because the image will be scaled down later
862        sBitmapOptionsCache.inPreferredConfig = Bitmap.Config.RGB_565;
863        sBitmapOptionsCache.inDither = false;
864
865        sBitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565;
866        sBitmapOptions.inDither = false;
867    }
868
869    public static void initAlbumArtCache() {
870        try {
871            int id = sService.getMediaMountedCount();
872            if (id != sArtCacheId) {
873                clearAlbumArtCache();
874                sArtCacheId = id;
875            }
876        } catch (RemoteException e) {
877            e.printStackTrace();
878        }
879    }
880
881    public static void clearAlbumArtCache() {
882        synchronized (sArtCache) {
883            sArtCache.clear();
884        }
885    }
886
887    public static Drawable getCachedArtwork(
888            Context context, long artIndex, BitmapDrawable defaultArtwork) {
889        Drawable d = null;
890        synchronized (sArtCache) {
891            d = sArtCache.get(artIndex);
892        }
893        if (d == null) {
894            d = defaultArtwork;
895            final Bitmap icon = defaultArtwork.getBitmap();
896            int w = icon.getWidth();
897            int h = icon.getHeight();
898            Bitmap b = MusicUtils.getArtworkQuick(context, artIndex, w, h);
899            if (b != null) {
900                d = new FastBitmapDrawable(b);
901                synchronized (sArtCache) {
902                    // the cache may have changed since we checked
903                    Drawable value = sArtCache.get(artIndex);
904                    if (value == null) {
905                        sArtCache.put(artIndex, d);
906                    } else {
907                        d = value;
908                    }
909                }
910            }
911        }
912        return d;
913    }
914
915    // Get album art for specified album. This method will not try to
916    // fall back to getting artwork directly from the file, nor will
917    // it attempt to repair the database.
918    private static Bitmap getArtworkQuick(Context context, long album_id, int w, int h) {
919        // NOTE: There is in fact a 1 pixel border on the right side in the ImageView
920        // used to display this drawable. Take it into account now, so we don't have to
921        // scale later.
922        w -= 1;
923        ContentResolver res = context.getContentResolver();
924        Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id);
925        if (uri != null) {
926            ParcelFileDescriptor fd = null;
927            try {
928                fd = res.openFileDescriptor(uri, "r");
929                int sampleSize = 1;
930
931                // Compute the closest power-of-two scale factor
932                // and pass that to sBitmapOptionsCache.inSampleSize, which will
933                // result in faster decoding and better quality
934                sBitmapOptionsCache.inJustDecodeBounds = true;
935                BitmapFactory.decodeFileDescriptor(
936                        fd.getFileDescriptor(), null, sBitmapOptionsCache);
937                int nextWidth = sBitmapOptionsCache.outWidth >> 1;
938                int nextHeight = sBitmapOptionsCache.outHeight >> 1;
939                while (nextWidth > w && nextHeight > h) {
940                    sampleSize <<= 1;
941                    nextWidth >>= 1;
942                    nextHeight >>= 1;
943                }
944
945                sBitmapOptionsCache.inSampleSize = sampleSize;
946                sBitmapOptionsCache.inJustDecodeBounds = false;
947                Bitmap b = BitmapFactory.decodeFileDescriptor(
948                        fd.getFileDescriptor(), null, sBitmapOptionsCache);
949
950                if (b != null) {
951                    // finally rescale to exactly the size we need
952                    if (sBitmapOptionsCache.outWidth != w || sBitmapOptionsCache.outHeight != h) {
953                        Bitmap tmp = Bitmap.createScaledBitmap(b, w, h, true);
954                        // Bitmap.createScaledBitmap() can return the same bitmap
955                        if (tmp != b) b.recycle();
956                        b = tmp;
957                    }
958                }
959
960                return b;
961            } catch (FileNotFoundException e) {
962            } finally {
963                try {
964                    if (fd != null) fd.close();
965                } catch (IOException e) {
966                }
967            }
968        }
969        return null;
970    }
971
972    /** Get album art for specified album. You should not pass in the album id
973     * for the "unknown" album here (use -1 instead)
974     * This method always returns the default album art icon when no album art is found.
975     */
976    public static Bitmap getArtwork(Context context, long song_id, long album_id) {
977        return getArtwork(context, song_id, album_id, true);
978    }
979
980    /** Get album art for specified album. You should not pass in the album id
981     * for the "unknown" album here (use -1 instead)
982     */
983    public static Bitmap getArtwork(
984            Context context, long song_id, long album_id, boolean allowdefault) {
985        if (album_id < 0) {
986            // This is something that is not in the database, so get the album art directly
987            // from the file.
988            if (song_id >= 0) {
989                Bitmap bm = getArtworkFromFile(context, song_id, -1);
990                if (bm != null) {
991                    return bm;
992                }
993            }
994            if (allowdefault) {
995                return getDefaultArtwork(context);
996            }
997            return null;
998        }
999
1000        ContentResolver res = context.getContentResolver();
1001        Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id);
1002        if (uri != null) {
1003            InputStream in = null;
1004            try {
1005                in = res.openInputStream(uri);
1006                return BitmapFactory.decodeStream(in, null, sBitmapOptions);
1007            } catch (FileNotFoundException ex) {
1008                // The album art thumbnail does not actually exist. Maybe the user deleted it, or
1009                // maybe it never existed to begin with.
1010                Bitmap bm = getArtworkFromFile(context, song_id, album_id);
1011                if (bm != null) {
1012                    if (bm.getConfig() == null) {
1013                        bm = bm.copy(Bitmap.Config.RGB_565, false);
1014                        if (bm == null && allowdefault) {
1015                            return getDefaultArtwork(context);
1016                        }
1017                    }
1018                } else if (allowdefault) {
1019                    bm = getDefaultArtwork(context);
1020                }
1021                return bm;
1022            } finally {
1023                try {
1024                    if (in != null) {
1025                        in.close();
1026                    }
1027                } catch (IOException ex) {
1028                }
1029            }
1030        }
1031
1032        return null;
1033    }
1034
1035    // get album art for specified file
1036    private static final String sExternalMediaUri =
1037            MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString();
1038    private static Bitmap getArtworkFromFile(Context context, long songid, long albumid) {
1039        Bitmap bm = null;
1040        byte[] art = null;
1041        String path = null;
1042
1043        if (albumid < 0 && songid < 0) {
1044            throw new IllegalArgumentException("Must specify an album or a song id");
1045        }
1046
1047        try {
1048            if (albumid < 0) {
1049                Uri uri = Uri.parse("content://media/external/audio/media/" + songid + "/albumart");
1050                ParcelFileDescriptor pfd =
1051                        context.getContentResolver().openFileDescriptor(uri, "r");
1052                if (pfd != null) {
1053                    FileDescriptor fd = pfd.getFileDescriptor();
1054                    bm = BitmapFactory.decodeFileDescriptor(fd);
1055                }
1056            } else {
1057                Uri uri = ContentUris.withAppendedId(sArtworkUri, albumid);
1058                ParcelFileDescriptor pfd =
1059                        context.getContentResolver().openFileDescriptor(uri, "r");
1060                if (pfd != null) {
1061                    FileDescriptor fd = pfd.getFileDescriptor();
1062                    bm = BitmapFactory.decodeFileDescriptor(fd);
1063                }
1064            }
1065        } catch (IllegalStateException ex) {
1066        } catch (FileNotFoundException ex) {
1067        }
1068        if (bm != null) {
1069            mCachedBit = bm;
1070        }
1071        return bm;
1072    }
1073
1074    private static Bitmap getDefaultArtwork(Context context) {
1075        BitmapFactory.Options opts = new BitmapFactory.Options();
1076        opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
1077        return BitmapFactory.decodeStream(
1078                context.getResources().openRawResource(R.drawable.albumart_mp_unknown), null, opts);
1079    }
1080
1081    static int getIntPref(Context context, String name, int def) {
1082        SharedPreferences prefs =
1083                context.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE);
1084        return prefs.getInt(name, def);
1085    }
1086
1087    static void setIntPref(Context context, String name, int value) {
1088        SharedPreferences prefs =
1089                context.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE);
1090        Editor ed = prefs.edit();
1091        ed.putInt(name, value);
1092        SharedPreferencesCompat.apply(ed);
1093    }
1094
1095    static void setRingtone(Context context, long id) {
1096        ContentResolver resolver = context.getContentResolver();
1097        // Set the flag in the database to mark this as a ringtone
1098        Uri ringUri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
1099        try {
1100            ContentValues values = new ContentValues(2);
1101            values.put(MediaStore.Audio.Media.IS_RINGTONE, "1");
1102            values.put(MediaStore.Audio.Media.IS_ALARM, "1");
1103            resolver.update(ringUri, values, null, null);
1104        } catch (UnsupportedOperationException ex) {
1105            // most likely the card just got unmounted
1106            Log.e(TAG, "couldn't set ringtone flag for id " + id);
1107            return;
1108        }
1109
1110        String[] cols = new String[] {MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DATA,
1111                MediaStore.Audio.Media.TITLE};
1112
1113        String where = MediaStore.Audio.Media._ID + "=" + id;
1114        Cursor cursor = query(
1115                context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, cols, where, null, null);
1116        try {
1117            if (cursor != null && cursor.getCount() == 1) {
1118                // Set the system setting to make this the current ringtone
1119                cursor.moveToFirst();
1120                Settings.System.putString(resolver, Settings.System.RINGTONE, ringUri.toString());
1121                String message = context.getString(R.string.ringtone_set, cursor.getString(2));
1122                Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
1123            }
1124        } finally {
1125            if (cursor != null) {
1126                cursor.close();
1127            }
1128        }
1129    }
1130
1131    static int sActiveTabIndex = -1;
1132
1133    static boolean updateButtonBar(Activity a, int highlight) {
1134        final TabWidget ll = (TabWidget) a.findViewById(R.id.buttonbar);
1135        boolean withtabs = false;
1136        Intent intent = a.getIntent();
1137        if (intent != null) {
1138            withtabs = intent.getBooleanExtra("withtabs", false);
1139        }
1140
1141        if (highlight == 0 || !withtabs) {
1142            ll.setVisibility(View.GONE);
1143            return withtabs;
1144        } else if (withtabs) {
1145            ll.setVisibility(View.VISIBLE);
1146        }
1147        for (int i = ll.getChildCount() - 1; i >= 0; i--) {
1148            View v = ll.getChildAt(i);
1149            boolean isActive = (v.getId() == highlight);
1150            if (isActive) {
1151                ll.setCurrentTab(i);
1152                sActiveTabIndex = i;
1153            }
1154            v.setTag(i);
1155            v.setOnFocusChangeListener(new View.OnFocusChangeListener() {
1156
1157                public void onFocusChange(View v, boolean hasFocus) {
1158                    if (hasFocus) {
1159                        for (int i = 0; i < ll.getTabCount(); i++) {
1160                            if (ll.getChildTabViewAt(i) == v) {
1161                                ll.setCurrentTab(i);
1162                                processTabClick((Activity) ll.getContext(), v,
1163                                        ll.getChildAt(sActiveTabIndex).getId());
1164                                break;
1165                            }
1166                        }
1167                    }
1168                }
1169            });
1170
1171            v.setOnClickListener(new View.OnClickListener() {
1172
1173                public void onClick(View v) {
1174                    processTabClick(
1175                            (Activity) ll.getContext(), v, ll.getChildAt(sActiveTabIndex).getId());
1176                }
1177            });
1178        }
1179        return withtabs;
1180    }
1181
1182    static void processTabClick(Activity a, View v, int current) {
1183        int id = v.getId();
1184        if (id == current) {
1185            return;
1186        }
1187
1188        final TabWidget ll = (TabWidget) a.findViewById(R.id.buttonbar);
1189
1190        activateTab(a, id);
1191        if (id != R.id.nowplayingtab) {
1192            ll.setCurrentTab((Integer) v.getTag());
1193            setIntPref(a, "activetab", id);
1194        }
1195    }
1196
1197    static void activateTab(Activity a, int id) {
1198        Intent intent = new Intent(Intent.ACTION_PICK);
1199        switch (id) {
1200            case R.id.artisttab:
1201                intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/artistalbum");
1202                break;
1203            case R.id.albumtab:
1204                intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/album");
1205                break;
1206            case R.id.songtab:
1207                intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
1208                break;
1209            case R.id.playlisttab:
1210                intent.setDataAndType(Uri.EMPTY, MediaStore.Audio.Playlists.CONTENT_TYPE);
1211                break;
1212            case R.id.nowplayingtab:
1213                intent = new Intent(a, MediaPlaybackActivity.class);
1214                a.startActivity(intent);
1215            // fall through and return
1216            default:
1217                return;
1218        }
1219        intent.putExtra("withtabs", true);
1220        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
1221        a.startActivity(intent);
1222        a.finish();
1223        a.overridePendingTransition(0, 0);
1224    }
1225
1226    static void updateNowPlaying(Activity a) {
1227        View nowPlayingView = a.findViewById(R.id.nowplaying);
1228        if (nowPlayingView == null) {
1229            return;
1230        }
1231        try {
1232            boolean withtabs = false;
1233            Intent intent = a.getIntent();
1234            if (intent != null) {
1235                withtabs = intent.getBooleanExtra("withtabs", false);
1236            }
1237            if (true && MusicUtils.sService != null && MusicUtils.sService.getAudioId() != -1) {
1238                TextView title = (TextView) nowPlayingView.findViewById(R.id.title);
1239                TextView artist = (TextView) nowPlayingView.findViewById(R.id.artist);
1240                title.setText(MusicUtils.sService.getTrackName());
1241                String artistName = MusicUtils.sService.getArtistName();
1242                if (MediaStore.UNKNOWN_STRING.equals(artistName)) {
1243                    artistName = a.getString(R.string.unknown_artist_name);
1244                }
1245                artist.setText(artistName);
1246                // mNowPlayingView.setOnFocusChangeListener(mFocuser);
1247                // mNowPlayingView.setOnClickListener(this);
1248                nowPlayingView.setVisibility(View.VISIBLE);
1249                nowPlayingView.setOnClickListener(new View.OnClickListener() {
1250
1251                    public void onClick(View v) {
1252                        Context c = v.getContext();
1253                        c.startActivity(new Intent(c, MediaPlaybackActivity.class));
1254                    }
1255                });
1256                return;
1257            }
1258        } catch (RemoteException ex) {
1259        }
1260        nowPlayingView.setVisibility(View.GONE);
1261    }
1262
1263    static void setBackground(View v, Bitmap bm) {
1264        if (bm == null) {
1265            v.setBackgroundResource(0);
1266            return;
1267        }
1268
1269        int vwidth = v.getWidth();
1270        int vheight = v.getHeight();
1271        int bwidth = bm.getWidth();
1272        int bheight = bm.getHeight();
1273        float scalex = (float) vwidth / bwidth;
1274        float scaley = (float) vheight / bheight;
1275        float scale = Math.max(scalex, scaley) * 1.3f;
1276
1277        Bitmap.Config config = Bitmap.Config.ARGB_8888;
1278        Bitmap bg = Bitmap.createBitmap(vwidth, vheight, config);
1279        Canvas c = new Canvas(bg);
1280        Paint paint = new Paint();
1281        paint.setAntiAlias(true);
1282        paint.setFilterBitmap(true);
1283        ColorMatrix greymatrix = new ColorMatrix();
1284        greymatrix.setSaturation(0);
1285        ColorMatrix darkmatrix = new ColorMatrix();
1286        darkmatrix.setScale(.3f, .3f, .3f, 1.0f);
1287        greymatrix.postConcat(darkmatrix);
1288        ColorFilter filter = new ColorMatrixColorFilter(greymatrix);
1289        paint.setColorFilter(filter);
1290        Matrix matrix = new Matrix();
1291        matrix.setTranslate(-bwidth / 2, -bheight / 2); // move bitmap center to origin
1292        matrix.postRotate(10);
1293        matrix.postScale(scale, scale);
1294        matrix.postTranslate(vwidth / 2, vheight / 2); // Move bitmap center to view center
1295        c.drawBitmap(bm, matrix, paint);
1296        v.setBackgroundDrawable(new BitmapDrawable(bg));
1297    }
1298
1299    static int getCardId(Context context) {
1300        ContentResolver res = context.getContentResolver();
1301        Cursor c = res.query(Uri.parse("content://media/external/fs_id"), null, null, null, null);
1302        int id = -1;
1303        if (c != null) {
1304            c.moveToFirst();
1305            id = c.getInt(0);
1306            c.close();
1307        }
1308        return id;
1309    }
1310
1311    static class LogEntry {
1312        Object item;
1313        long time;
1314
1315        LogEntry(Object o) {
1316            item = o;
1317            time = System.currentTimeMillis();
1318        }
1319
1320        void dump(PrintWriter out) {
1321            sTime.set(time);
1322            out.print(sTime.toString() + " : ");
1323            if (item instanceof Exception) {
1324                ((Exception) item).printStackTrace(out);
1325            } else {
1326                out.println(item);
1327            }
1328        }
1329    }
1330
1331    private static LogEntry[] sMusicLog = new LogEntry[100];
1332    private static int sLogPtr = 0;
1333    private static Time sTime = new Time();
1334
1335    static void debugLog(Object o) {
1336        sMusicLog[sLogPtr] = new LogEntry(o);
1337        sLogPtr++;
1338        if (sLogPtr >= sMusicLog.length) {
1339            sLogPtr = 0;
1340        }
1341    }
1342
1343    static void debugDump(PrintWriter out) {
1344        for (int i = 0; i < sMusicLog.length; i++) {
1345            int idx = (sLogPtr + i);
1346            if (idx >= sMusicLog.length) {
1347                idx -= sMusicLog.length;
1348            }
1349            LogEntry entry = sMusicLog[idx];
1350            if (entry != null) {
1351                entry.dump(out);
1352            }
1353        }
1354    }
1355}
1356