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