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