blob: e96c39382c5996e7b5866c2829b68795b6d7823f [file] [log] [blame]
The Android Open Source Project792a2202009-03-03 19:32:30 -08001/*
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
The Android Open Source Project792a2202009-03-03 19:32:30 -080019import android.app.Activity;
The Android Open Source Project792a2202009-03-03 19:32:30 -080020import android.content.ComponentName;
21import android.content.ContentResolver;
22import android.content.ContentUris;
23import android.content.ContentValues;
24import android.content.Context;
25import android.content.Intent;
26import android.content.ServiceConnection;
27import android.content.SharedPreferences;
28import android.content.SharedPreferences.Editor;
29import android.content.res.Resources;
30import android.database.Cursor;
31import android.graphics.Bitmap;
32import android.graphics.BitmapFactory;
33import android.graphics.Canvas;
34import android.graphics.ColorFilter;
35import android.graphics.PixelFormat;
36import android.graphics.drawable.BitmapDrawable;
37import android.graphics.drawable.Drawable;
The Android Open Source Project792a2202009-03-03 19:32:30 -080038import android.net.Uri;
The Android Open Source Project792a2202009-03-03 19:32:30 -080039import android.os.Environment;
40import android.os.ParcelFileDescriptor;
Marco Nelissen7a16cc72009-06-16 08:54:17 -070041import android.os.RemoteException;
The Android Open Source Project792a2202009-03-03 19:32:30 -080042import android.provider.MediaStore;
43import android.provider.Settings;
44import android.util.Log;
45import android.view.SubMenu;
46import android.view.View;
47import android.view.Window;
48import android.widget.TextView;
49import android.widget.Toast;
50
Marco Nelissen7a16cc72009-06-16 08:54:17 -070051import java.io.File;
52import java.io.FileDescriptor;
53import java.io.FileNotFoundException;
54import java.io.IOException;
55import java.io.InputStream;
56import java.util.Arrays;
57import java.util.Formatter;
58import java.util.HashMap;
59import java.util.Locale;
60
The Android Open Source Project792a2202009-03-03 19:32:30 -080061public class MusicUtils {
62
63 private static final String TAG = "MusicUtils";
64
65 public interface Defs {
66 public final static int OPEN_URL = 0;
67 public final static int ADD_TO_PLAYLIST = 1;
68 public final static int USE_AS_RINGTONE = 2;
69 public final static int PLAYLIST_SELECTED = 3;
70 public final static int NEW_PLAYLIST = 4;
71 public final static int PLAY_SELECTION = 5;
72 public final static int GOTO_START = 6;
73 public final static int GOTO_PLAYBACK = 7;
74 public final static int PARTY_SHUFFLE = 8;
75 public final static int SHUFFLE_ALL = 9;
76 public final static int DELETE_ITEM = 10;
77 public final static int SCAN_DONE = 11;
78 public final static int QUEUE = 12;
79 public final static int CHILD_MENU_BASE = 13; // this should be the last item
80 }
81
82 public static String makeAlbumsLabel(Context context, int numalbums, int numsongs, boolean isUnknown) {
83 // There are two formats for the albums/songs information:
84 // "N Song(s)" - used for unknown artist/album
85 // "N Album(s)" - used for known albums
86
87 StringBuilder songs_albums = new StringBuilder();
88
89 Resources r = context.getResources();
90 if (isUnknown) {
91 if (numsongs == 1) {
92 songs_albums.append(context.getString(R.string.onesong));
93 } else {
94 String f = r.getQuantityText(R.plurals.Nsongs, numsongs).toString();
95 sFormatBuilder.setLength(0);
96 sFormatter.format(f, Integer.valueOf(numsongs));
97 songs_albums.append(sFormatBuilder);
98 }
99 } else {
100 String f = r.getQuantityText(R.plurals.Nalbums, numalbums).toString();
101 sFormatBuilder.setLength(0);
102 sFormatter.format(f, Integer.valueOf(numalbums));
103 songs_albums.append(sFormatBuilder);
104 songs_albums.append(context.getString(R.string.albumsongseparator));
105 }
106 return songs_albums.toString();
107 }
108
109 /**
110 * This is now only used for the query screen
111 */
112 public static String makeAlbumsSongsLabel(Context context, int numalbums, int numsongs, boolean isUnknown) {
113 // There are several formats for the albums/songs information:
114 // "1 Song" - used if there is only 1 song
115 // "N Songs" - used for the "unknown artist" item
116 // "1 Album"/"N Songs"
117 // "N Album"/"M Songs"
118 // Depending on locale, these may need to be further subdivided
119
120 StringBuilder songs_albums = new StringBuilder();
121
122 if (numsongs == 1) {
123 songs_albums.append(context.getString(R.string.onesong));
124 } else {
125 Resources r = context.getResources();
126 if (! isUnknown) {
127 String f = r.getQuantityText(R.plurals.Nalbums, numalbums).toString();
128 sFormatBuilder.setLength(0);
129 sFormatter.format(f, Integer.valueOf(numalbums));
130 songs_albums.append(sFormatBuilder);
131 songs_albums.append(context.getString(R.string.albumsongseparator));
132 }
133 String f = r.getQuantityText(R.plurals.Nsongs, numsongs).toString();
134 sFormatBuilder.setLength(0);
135 sFormatter.format(f, Integer.valueOf(numsongs));
136 songs_albums.append(sFormatBuilder);
137 }
138 return songs_albums.toString();
139 }
140
141 public static IMediaPlaybackService sService = null;
142 private static HashMap<Context, ServiceBinder> sConnectionMap = new HashMap<Context, ServiceBinder>();
143
144 public static boolean bindToService(Context context) {
145 return bindToService(context, null);
146 }
147
148 public static boolean bindToService(Context context, ServiceConnection callback) {
149 context.startService(new Intent(context, MediaPlaybackService.class));
150 ServiceBinder sb = new ServiceBinder(callback);
151 sConnectionMap.put(context, sb);
152 return context.bindService((new Intent()).setClass(context,
153 MediaPlaybackService.class), sb, 0);
154 }
155
156 public static void unbindFromService(Context context) {
157 ServiceBinder sb = (ServiceBinder) sConnectionMap.remove(context);
158 if (sb == null) {
159 Log.e("MusicUtils", "Trying to unbind for unknown Context");
160 return;
161 }
162 context.unbindService(sb);
163 if (sConnectionMap.isEmpty()) {
164 // presumably there is nobody interested in the service at this point,
165 // so don't hang on to the ServiceConnection
166 sService = null;
167 }
168 }
169
170 private static class ServiceBinder implements ServiceConnection {
171 ServiceConnection mCallback;
172 ServiceBinder(ServiceConnection callback) {
173 mCallback = callback;
174 }
175
176 public void onServiceConnected(ComponentName className, android.os.IBinder service) {
177 sService = IMediaPlaybackService.Stub.asInterface(service);
178 initAlbumArtCache();
179 if (mCallback != null) {
180 mCallback.onServiceConnected(className, service);
181 }
182 }
183
184 public void onServiceDisconnected(ComponentName className) {
185 if (mCallback != null) {
186 mCallback.onServiceDisconnected(className);
187 }
188 sService = null;
189 }
190 }
191
Marco Nelissenbd447b62009-06-29 14:52:05 -0700192 public static long getCurrentAlbumId() {
The Android Open Source Project792a2202009-03-03 19:32:30 -0800193 if (sService != null) {
194 try {
195 return sService.getAlbumId();
196 } catch (RemoteException ex) {
197 }
198 }
199 return -1;
200 }
201
Marco Nelissenbd447b62009-06-29 14:52:05 -0700202 public static long getCurrentArtistId() {
The Android Open Source Project792a2202009-03-03 19:32:30 -0800203 if (MusicUtils.sService != null) {
204 try {
205 return sService.getArtistId();
206 } catch (RemoteException ex) {
207 }
208 }
209 return -1;
210 }
211
Marco Nelissenbd447b62009-06-29 14:52:05 -0700212 public static long getCurrentAudioId() {
The Android Open Source Project792a2202009-03-03 19:32:30 -0800213 if (MusicUtils.sService != null) {
214 try {
215 return sService.getAudioId();
216 } catch (RemoteException ex) {
217 }
218 }
219 return -1;
220 }
221
222 public static int getCurrentShuffleMode() {
223 int mode = MediaPlaybackService.SHUFFLE_NONE;
224 if (sService != null) {
225 try {
226 mode = sService.getShuffleMode();
227 } catch (RemoteException ex) {
228 }
229 }
230 return mode;
231 }
232
233 /*
234 * Returns true if a file is currently opened for playback (regardless
235 * of whether it's playing or paused).
236 */
237 public static boolean isMusicLoaded() {
238 if (MusicUtils.sService != null) {
239 try {
240 return sService.getPath() != null;
241 } catch (RemoteException ex) {
242 }
243 }
244 return false;
245 }
246
Marco Nelissenbd447b62009-06-29 14:52:05 -0700247 private final static long [] sEmptyList = new long[0];
The Android Open Source Project792a2202009-03-03 19:32:30 -0800248
Marco Nelissenbd447b62009-06-29 14:52:05 -0700249 public static long [] getSongListForCursor(Cursor cursor) {
The Android Open Source Project792a2202009-03-03 19:32:30 -0800250 if (cursor == null) {
251 return sEmptyList;
252 }
253 int len = cursor.getCount();
Marco Nelissenbd447b62009-06-29 14:52:05 -0700254 long [] list = new long[len];
The Android Open Source Project792a2202009-03-03 19:32:30 -0800255 cursor.moveToFirst();
256 int colidx = -1;
257 try {
258 colidx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
259 } catch (IllegalArgumentException ex) {
260 colidx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
261 }
262 for (int i = 0; i < len; i++) {
Marco Nelissenbd447b62009-06-29 14:52:05 -0700263 list[i] = cursor.getLong(colidx);
The Android Open Source Project792a2202009-03-03 19:32:30 -0800264 cursor.moveToNext();
265 }
266 return list;
267 }
268
Marco Nelissenbd447b62009-06-29 14:52:05 -0700269 public static long [] getSongListForArtist(Context context, long id) {
The Android Open Source Project792a2202009-03-03 19:32:30 -0800270 final String[] ccols = new String[] { MediaStore.Audio.Media._ID };
271 String where = MediaStore.Audio.Media.ARTIST_ID + "=" + id + " AND " +
272 MediaStore.Audio.Media.IS_MUSIC + "=1";
273 Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
274 ccols, where, null,
275 MediaStore.Audio.Media.ALBUM_KEY + "," + MediaStore.Audio.Media.TRACK);
276
277 if (cursor != null) {
Marco Nelissenbd447b62009-06-29 14:52:05 -0700278 long [] list = getSongListForCursor(cursor);
The Android Open Source Project792a2202009-03-03 19:32:30 -0800279 cursor.close();
280 return list;
281 }
282 return sEmptyList;
283 }
284
Marco Nelissenbd447b62009-06-29 14:52:05 -0700285 public static long [] getSongListForAlbum(Context context, long id) {
The Android Open Source Project792a2202009-03-03 19:32:30 -0800286 final String[] ccols = new String[] { MediaStore.Audio.Media._ID };
287 String where = MediaStore.Audio.Media.ALBUM_ID + "=" + id + " AND " +
288 MediaStore.Audio.Media.IS_MUSIC + "=1";
289 Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
290 ccols, where, null, MediaStore.Audio.Media.TRACK);
291
292 if (cursor != null) {
Marco Nelissenbd447b62009-06-29 14:52:05 -0700293 long [] list = getSongListForCursor(cursor);
The Android Open Source Project792a2202009-03-03 19:32:30 -0800294 cursor.close();
295 return list;
296 }
297 return sEmptyList;
298 }
299
Marco Nelissenbd447b62009-06-29 14:52:05 -0700300 public static long [] getSongListForPlaylist(Context context, long plid) {
The Android Open Source Project792a2202009-03-03 19:32:30 -0800301 final String[] ccols = new String[] { MediaStore.Audio.Playlists.Members.AUDIO_ID };
302 Cursor cursor = query(context, MediaStore.Audio.Playlists.Members.getContentUri("external", plid),
303 ccols, null, null, MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);
304
305 if (cursor != null) {
Marco Nelissenbd447b62009-06-29 14:52:05 -0700306 long [] list = getSongListForCursor(cursor);
The Android Open Source Project792a2202009-03-03 19:32:30 -0800307 cursor.close();
308 return list;
309 }
310 return sEmptyList;
311 }
312
313 public static void playPlaylist(Context context, long plid) {
Marco Nelissenbd447b62009-06-29 14:52:05 -0700314 long [] list = getSongListForPlaylist(context, plid);
The Android Open Source Project792a2202009-03-03 19:32:30 -0800315 if (list != null) {
316 playAll(context, list, -1, false);
317 }
318 }
319
Marco Nelissenbd447b62009-06-29 14:52:05 -0700320 public static long [] getAllSongs(Context context) {
The Android Open Source Project792a2202009-03-03 19:32:30 -0800321 Cursor c = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
322 new String[] {MediaStore.Audio.Media._ID}, MediaStore.Audio.Media.IS_MUSIC + "=1",
323 null, null);
324 try {
325 if (c == null || c.getCount() == 0) {
326 return null;
327 }
328 int len = c.getCount();
Marco Nelissenbd447b62009-06-29 14:52:05 -0700329 long [] list = new long[len];
The Android Open Source Project792a2202009-03-03 19:32:30 -0800330 for (int i = 0; i < len; i++) {
331 c.moveToNext();
Marco Nelissenbd447b62009-06-29 14:52:05 -0700332 list[i] = c.getLong(0);
The Android Open Source Project792a2202009-03-03 19:32:30 -0800333 }
334
335 return list;
336 } finally {
337 if (c != null) {
338 c.close();
339 }
340 }
341 }
342
343 /**
344 * Fills out the given submenu with items for "new playlist" and
345 * any existing playlists. When the user selects an item, the
346 * application will receive PLAYLIST_SELECTED with the Uri of
347 * the selected playlist, NEW_PLAYLIST if a new playlist
348 * should be created, and QUEUE if the "current playlist" was
349 * selected.
350 * @param context The context to use for creating the menu items
351 * @param sub The submenu to add the items to.
352 */
353 public static void makePlaylistMenu(Context context, SubMenu sub) {
354 String[] cols = new String[] {
355 MediaStore.Audio.Playlists._ID,
356 MediaStore.Audio.Playlists.NAME
357 };
358 ContentResolver resolver = context.getContentResolver();
359 if (resolver == null) {
360 System.out.println("resolver = null");
361 } else {
362 String whereclause = MediaStore.Audio.Playlists.NAME + " != ''";
363 Cursor cur = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
364 cols, whereclause, null,
365 MediaStore.Audio.Playlists.NAME);
366 sub.clear();
367 sub.add(1, Defs.QUEUE, 0, R.string.queue);
368 sub.add(1, Defs.NEW_PLAYLIST, 0, R.string.new_playlist);
369 if (cur != null && cur.getCount() > 0) {
370 //sub.addSeparator(1, 0);
371 cur.moveToFirst();
372 while (! cur.isAfterLast()) {
373 Intent intent = new Intent();
Marco Nelissenbd447b62009-06-29 14:52:05 -0700374 intent.putExtra("playlist", cur.getLong(0));
The Android Open Source Project792a2202009-03-03 19:32:30 -0800375// if (cur.getInt(0) == mLastPlaylistSelected) {
376// sub.add(0, MusicBaseActivity.PLAYLIST_SELECTED, cur.getString(1)).setIntent(intent);
377// } else {
378 sub.add(1, Defs.PLAYLIST_SELECTED, 0, cur.getString(1)).setIntent(intent);
379// }
380 cur.moveToNext();
381 }
382 }
383 if (cur != null) {
384 cur.close();
385 }
386 }
387 }
388
389 public static void clearPlaylist(Context context, int plid) {
390
391 Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", plid);
392 context.getContentResolver().delete(uri, null, null);
393 return;
394 }
395
Marco Nelissenbd447b62009-06-29 14:52:05 -0700396 public static void deleteTracks(Context context, long [] list) {
The Android Open Source Project792a2202009-03-03 19:32:30 -0800397
398 String [] cols = new String [] { MediaStore.Audio.Media._ID,
399 MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.ALBUM_ID };
400 StringBuilder where = new StringBuilder();
401 where.append(MediaStore.Audio.Media._ID + " IN (");
402 for (int i = 0; i < list.length; i++) {
403 where.append(list[i]);
404 if (i < list.length - 1) {
405 where.append(",");
406 }
407 }
408 where.append(")");
409 Cursor c = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, cols,
410 where.toString(), null, null);
411
412 if (c != null) {
413
414 // step 1: remove selected tracks from the current playlist, as well
415 // as from the album art cache
416 try {
417 c.moveToFirst();
418 while (! c.isAfterLast()) {
419 // remove from current playlist
Marco Nelissenbd447b62009-06-29 14:52:05 -0700420 long id = c.getLong(0);
The Android Open Source Project792a2202009-03-03 19:32:30 -0800421 sService.removeTrack(id);
422 // remove from album art cache
Marco Nelissenbd447b62009-06-29 14:52:05 -0700423 long artIndex = c.getLong(2);
The Android Open Source Project792a2202009-03-03 19:32:30 -0800424 synchronized(sArtCache) {
425 sArtCache.remove(artIndex);
426 }
427 c.moveToNext();
428 }
429 } catch (RemoteException ex) {
430 }
431
432 // step 2: remove selected tracks from the database
433 context.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, where.toString(), null);
434
435 // step 3: remove files from card
436 c.moveToFirst();
437 while (! c.isAfterLast()) {
438 String name = c.getString(1);
439 File f = new File(name);
440 try { // File.delete can throw a security exception
441 if (!f.delete()) {
442 // I'm not sure if we'd ever get here (deletion would
443 // have to fail, but no exception thrown)
444 Log.e("MusicUtils", "Failed to delete file " + name);
445 }
446 c.moveToNext();
447 } catch (SecurityException ex) {
448 c.moveToNext();
449 }
450 }
451 c.close();
452 }
453
454 String message = context.getResources().getQuantityString(
455 R.plurals.NNNtracksdeleted, list.length, Integer.valueOf(list.length));
456
457 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
458 // We deleted a number of tracks, which could affect any number of things
459 // in the media content domain, so update everything.
460 context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
461 }
462
Marco Nelissenbd447b62009-06-29 14:52:05 -0700463 public static void addToCurrentPlaylist(Context context, long [] list) {
The Android Open Source Project792a2202009-03-03 19:32:30 -0800464 if (sService == null) {
465 return;
466 }
467 try {
468 sService.enqueue(list, MediaPlaybackService.LAST);
469 String message = context.getResources().getQuantityString(
470 R.plurals.NNNtrackstoplaylist, list.length, Integer.valueOf(list.length));
471 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
472 } catch (RemoteException ex) {
473 }
474 }
475
Marco Nelissenbd447b62009-06-29 14:52:05 -0700476 public static void addToPlaylist(Context context, long [] ids, long playlistid) {
The Android Open Source Project792a2202009-03-03 19:32:30 -0800477 if (ids == null) {
478 // this shouldn't happen (the menuitems shouldn't be visible
479 // unless the selected item represents something playable
480 Log.e("MusicBase", "ListSelection null");
481 } else {
482 int size = ids.length;
483 ContentValues values [] = new ContentValues[size];
484 ContentResolver resolver = context.getContentResolver();
485 // need to determine the number of items currently in the playlist,
486 // so the play_order field can be maintained.
487 String[] cols = new String[] {
488 "count(*)"
489 };
490 Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid);
491 Cursor cur = resolver.query(uri, cols, null, null, null);
492 cur.moveToFirst();
493 int base = cur.getInt(0);
494 cur.close();
495
496 for (int i = 0; i < size; i++) {
497 values[i] = new ContentValues();
498 values[i].put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(base + i));
499 values[i].put(MediaStore.Audio.Playlists.Members.AUDIO_ID, ids[i]);
500 }
501 resolver.bulkInsert(uri, values);
502 String message = context.getResources().getQuantityString(
503 R.plurals.NNNtrackstoplaylist, size, Integer.valueOf(size));
504 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
505 //mLastPlaylistSelected = playlistid;
506 }
507 }
508
509 public static Cursor query(Context context, Uri uri, String[] projection,
Marco Nelissen4248ed22009-07-30 08:28:49 -0700510 String selection, String[] selectionArgs, String sortOrder, int limit) {
The Android Open Source Project792a2202009-03-03 19:32:30 -0800511 try {
512 ContentResolver resolver = context.getContentResolver();
513 if (resolver == null) {
514 return null;
515 }
Marco Nelissen4248ed22009-07-30 08:28:49 -0700516 if (limit > 0) {
517 uri = uri.buildUpon().appendQueryParameter("limit", "" + limit).build();
518 }
The Android Open Source Project792a2202009-03-03 19:32:30 -0800519 return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
520 } catch (UnsupportedOperationException ex) {
521 return null;
522 }
523
524 }
Marco Nelissen4248ed22009-07-30 08:28:49 -0700525 public static Cursor query(Context context, Uri uri, String[] projection,
526 String selection, String[] selectionArgs, String sortOrder) {
527 return query(context, uri, projection, selection, selectionArgs, sortOrder, 0);
528 }
The Android Open Source Project792a2202009-03-03 19:32:30 -0800529
530 public static boolean isMediaScannerScanning(Context context) {
531 boolean result = false;
532 Cursor cursor = query(context, MediaStore.getMediaScannerUri(),
533 new String [] { MediaStore.MEDIA_SCANNER_VOLUME }, null, null, null);
534 if (cursor != null) {
535 if (cursor.getCount() == 1) {
536 cursor.moveToFirst();
537 result = "external".equals(cursor.getString(0));
538 }
539 cursor.close();
540 }
541
542 return result;
543 }
544
545 public static void setSpinnerState(Activity a) {
546 if (isMediaScannerScanning(a)) {
547 // start the progress spinner
548 a.getWindow().setFeatureInt(
549 Window.FEATURE_INDETERMINATE_PROGRESS,
550 Window.PROGRESS_INDETERMINATE_ON);
551
552 a.getWindow().setFeatureInt(
553 Window.FEATURE_INDETERMINATE_PROGRESS,
554 Window.PROGRESS_VISIBILITY_ON);
555 } else {
556 // stop the progress spinner
557 a.getWindow().setFeatureInt(
558 Window.FEATURE_INDETERMINATE_PROGRESS,
559 Window.PROGRESS_VISIBILITY_OFF);
560 }
561 }
562
563 public static void displayDatabaseError(Activity a) {
564 String status = Environment.getExternalStorageState();
565 int title = R.string.sdcard_error_title;
566 int message = R.string.sdcard_error_message;
567
568 if (status.equals(Environment.MEDIA_SHARED) ||
569 status.equals(Environment.MEDIA_UNMOUNTED)) {
570 title = R.string.sdcard_busy_title;
571 message = R.string.sdcard_busy_message;
572 } else if (status.equals(Environment.MEDIA_REMOVED)) {
573 title = R.string.sdcard_missing_title;
574 message = R.string.sdcard_missing_message;
575 } else if (status.equals(Environment.MEDIA_MOUNTED)){
576 // The card is mounted, but we didn't get a valid cursor.
577 // This probably means the mediascanner hasn't started scanning the
578 // card yet (there is a small window of time during boot where this
579 // will happen).
580 a.setTitle("");
581 Intent intent = new Intent();
582 intent.setClass(a, ScanningProgress.class);
583 a.startActivityForResult(intent, Defs.SCAN_DONE);
584 } else {
585 Log.d(TAG, "sd card: " + status);
586 }
587
588 a.setTitle(title);
589 View v = a.findViewById(R.id.sd_message);
590 if (v != null) {
591 v.setVisibility(View.VISIBLE);
592 }
593 v = a.findViewById(R.id.sd_icon);
594 if (v != null) {
595 v.setVisibility(View.VISIBLE);
596 }
597 v = a.findViewById(android.R.id.list);
598 if (v != null) {
599 v.setVisibility(View.GONE);
600 }
601 TextView tv = (TextView) a.findViewById(R.id.sd_message);
602 tv.setText(message);
603 }
604
605 public static void hideDatabaseError(Activity a) {
606 View v = a.findViewById(R.id.sd_message);
607 if (v != null) {
608 v.setVisibility(View.GONE);
609 }
610 v = a.findViewById(R.id.sd_icon);
611 if (v != null) {
612 v.setVisibility(View.GONE);
613 }
614 v = a.findViewById(android.R.id.list);
615 if (v != null) {
616 v.setVisibility(View.VISIBLE);
617 }
618 }
619
620 static protected Uri getContentURIForPath(String path) {
621 return Uri.fromFile(new File(path));
622 }
623
624
625 /* Try to use String.format() as little as possible, because it creates a
626 * new Formatter every time you call it, which is very inefficient.
627 * Reusing an existing Formatter more than tripled the speed of
628 * makeTimeString().
629 * This Formatter/StringBuilder are also used by makeAlbumSongsLabel()
630 */
631 private static StringBuilder sFormatBuilder = new StringBuilder();
632 private static Formatter sFormatter = new Formatter(sFormatBuilder, Locale.getDefault());
633 private static final Object[] sTimeArgs = new Object[5];
634
635 public static String makeTimeString(Context context, long secs) {
Marco Nelissen1e5a5672009-09-02 11:09:15 -0700636 String durationformat = context.getString(
637 secs < 3600 ? R.string.durationformatshort : R.string.durationformatlong);
The Android Open Source Project792a2202009-03-03 19:32:30 -0800638
639 /* Provide multiple arguments so the format can be changed easily
640 * by modifying the xml.
641 */
642 sFormatBuilder.setLength(0);
643
644 final Object[] timeArgs = sTimeArgs;
645 timeArgs[0] = secs / 3600;
646 timeArgs[1] = secs / 60;
647 timeArgs[2] = (secs / 60) % 60;
648 timeArgs[3] = secs;
649 timeArgs[4] = secs % 60;
650
651 return sFormatter.format(durationformat, timeArgs).toString();
652 }
653
654 public static void shuffleAll(Context context, Cursor cursor) {
655 playAll(context, cursor, 0, true);
656 }
657
658 public static void playAll(Context context, Cursor cursor) {
659 playAll(context, cursor, 0, false);
660 }
661
662 public static void playAll(Context context, Cursor cursor, int position) {
663 playAll(context, cursor, position, false);
664 }
665
Marco Nelissenbd447b62009-06-29 14:52:05 -0700666 public static void playAll(Context context, long [] list, int position) {
The Android Open Source Project792a2202009-03-03 19:32:30 -0800667 playAll(context, list, position, false);
668 }
669
670 private static void playAll(Context context, Cursor cursor, int position, boolean force_shuffle) {
671
Marco Nelissenbd447b62009-06-29 14:52:05 -0700672 long [] list = getSongListForCursor(cursor);
The Android Open Source Project792a2202009-03-03 19:32:30 -0800673 playAll(context, list, position, force_shuffle);
674 }
675
Marco Nelissenbd447b62009-06-29 14:52:05 -0700676 private static void playAll(Context context, long [] list, int position, boolean force_shuffle) {
The Android Open Source Project792a2202009-03-03 19:32:30 -0800677 if (list.length == 0 || sService == null) {
678 Log.d("MusicUtils", "attempt to play empty song list");
679 // Don't try to play empty playlists. Nothing good will come of it.
680 String message = context.getString(R.string.emptyplaylist, list.length);
681 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
682 return;
683 }
684 try {
685 if (force_shuffle) {
686 sService.setShuffleMode(MediaPlaybackService.SHUFFLE_NORMAL);
687 }
Marco Nelissenbd447b62009-06-29 14:52:05 -0700688 long curid = sService.getAudioId();
The Android Open Source Project792a2202009-03-03 19:32:30 -0800689 int curpos = sService.getQueuePosition();
690 if (position != -1 && curpos == position && curid == list[position]) {
691 // The selected file is the file that's currently playing;
692 // figure out if we need to restart with a new playlist,
693 // or just launch the playback activity.
Marco Nelissenbd447b62009-06-29 14:52:05 -0700694 long [] playlist = sService.getQueue();
The Android Open Source Project792a2202009-03-03 19:32:30 -0800695 if (Arrays.equals(list, playlist)) {
696 // we don't need to set a new list, but we should resume playback if needed
697 sService.play();
698 return; // the 'finally' block will still run
699 }
700 }
701 if (position < 0) {
702 position = 0;
703 }
704 sService.open(list, force_shuffle ? -1 : position);
705 sService.play();
706 } catch (RemoteException ex) {
707 } finally {
708 Intent intent = new Intent("com.android.music.PLAYBACK_VIEWER")
709 .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
710 context.startActivity(intent);
711 }
712 }
713
714 public static void clearQueue() {
715 try {
716 sService.removeTracks(0, Integer.MAX_VALUE);
717 } catch (RemoteException ex) {
718 }
719 }
720
721 // A really simple BitmapDrawable-like class, that doesn't do
722 // scaling, dithering or filtering.
723 private static class FastBitmapDrawable extends Drawable {
724 private Bitmap mBitmap;
725 public FastBitmapDrawable(Bitmap b) {
726 mBitmap = b;
727 }
728 @Override
729 public void draw(Canvas canvas) {
730 canvas.drawBitmap(mBitmap, 0, 0, null);
731 }
732 @Override
733 public int getOpacity() {
734 return PixelFormat.OPAQUE;
735 }
736 @Override
737 public void setAlpha(int alpha) {
738 }
739 @Override
740 public void setColorFilter(ColorFilter cf) {
741 }
742 }
743
744 private static int sArtId = -2;
The Android Open Source Project792a2202009-03-03 19:32:30 -0800745 private static Bitmap mCachedBit = null;
746 private static final BitmapFactory.Options sBitmapOptionsCache = new BitmapFactory.Options();
747 private static final BitmapFactory.Options sBitmapOptions = new BitmapFactory.Options();
748 private static final Uri sArtworkUri = Uri.parse("content://media/external/audio/albumart");
Marco Nelissenbd447b62009-06-29 14:52:05 -0700749 private static final HashMap<Long, Drawable> sArtCache = new HashMap<Long, Drawable>();
The Android Open Source Project792a2202009-03-03 19:32:30 -0800750 private static int sArtCacheId = -1;
751
752 static {
753 // for the cache,
754 // 565 is faster to decode and display
755 // and we don't want to dither here because the image will be scaled down later
756 sBitmapOptionsCache.inPreferredConfig = Bitmap.Config.RGB_565;
757 sBitmapOptionsCache.inDither = false;
758
759 sBitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565;
760 sBitmapOptions.inDither = false;
761 }
762
763 public static void initAlbumArtCache() {
764 try {
765 int id = sService.getMediaMountedCount();
766 if (id != sArtCacheId) {
767 clearAlbumArtCache();
768 sArtCacheId = id;
769 }
770 } catch (RemoteException e) {
771 e.printStackTrace();
772 }
773 }
774
775 public static void clearAlbumArtCache() {
776 synchronized(sArtCache) {
777 sArtCache.clear();
778 }
779 }
780
Marco Nelissenbd447b62009-06-29 14:52:05 -0700781 public static Drawable getCachedArtwork(Context context, long artIndex, BitmapDrawable defaultArtwork) {
The Android Open Source Project792a2202009-03-03 19:32:30 -0800782 Drawable d = null;
783 synchronized(sArtCache) {
784 d = sArtCache.get(artIndex);
785 }
786 if (d == null) {
787 d = defaultArtwork;
788 final Bitmap icon = defaultArtwork.getBitmap();
789 int w = icon.getWidth();
790 int h = icon.getHeight();
791 Bitmap b = MusicUtils.getArtworkQuick(context, artIndex, w, h);
792 if (b != null) {
793 d = new FastBitmapDrawable(b);
794 synchronized(sArtCache) {
795 // the cache may have changed since we checked
796 Drawable value = sArtCache.get(artIndex);
797 if (value == null) {
798 sArtCache.put(artIndex, d);
799 } else {
800 d = value;
801 }
802 }
803 }
804 }
805 return d;
806 }
807
808 // Get album art for specified album. This method will not try to
809 // fall back to getting artwork directly from the file, nor will
810 // it attempt to repair the database.
Marco Nelissenbd447b62009-06-29 14:52:05 -0700811 private static Bitmap getArtworkQuick(Context context, long album_id, int w, int h) {
The Android Open Source Project792a2202009-03-03 19:32:30 -0800812 // NOTE: There is in fact a 1 pixel border on the right side in the ImageView
813 // used to display this drawable. Take it into account now, so we don't have to
814 // scale later.
815 w -= 1;
816 ContentResolver res = context.getContentResolver();
817 Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id);
818 if (uri != null) {
819 ParcelFileDescriptor fd = null;
820 try {
821 fd = res.openFileDescriptor(uri, "r");
822 int sampleSize = 1;
823
824 // Compute the closest power-of-two scale factor
825 // and pass that to sBitmapOptionsCache.inSampleSize, which will
826 // result in faster decoding and better quality
827 sBitmapOptionsCache.inJustDecodeBounds = true;
828 BitmapFactory.decodeFileDescriptor(
829 fd.getFileDescriptor(), null, sBitmapOptionsCache);
830 int nextWidth = sBitmapOptionsCache.outWidth >> 1;
831 int nextHeight = sBitmapOptionsCache.outHeight >> 1;
832 while (nextWidth>w && nextHeight>h) {
833 sampleSize <<= 1;
834 nextWidth >>= 1;
835 nextHeight >>= 1;
836 }
837
838 sBitmapOptionsCache.inSampleSize = sampleSize;
839 sBitmapOptionsCache.inJustDecodeBounds = false;
840 Bitmap b = BitmapFactory.decodeFileDescriptor(
841 fd.getFileDescriptor(), null, sBitmapOptionsCache);
842
843 if (b != null) {
844 // finally rescale to exactly the size we need
845 if (sBitmapOptionsCache.outWidth != w || sBitmapOptionsCache.outHeight != h) {
846 Bitmap tmp = Bitmap.createScaledBitmap(b, w, h, true);
847 // Bitmap.createScaledBitmap() can return the same bitmap
848 if (tmp != b) b.recycle();
849 b = tmp;
850 }
851 }
852
853 return b;
854 } catch (FileNotFoundException e) {
855 } finally {
856 try {
857 if (fd != null)
858 fd.close();
859 } catch (IOException e) {
860 }
861 }
862 }
863 return null;
864 }
865
866 /** Get album art for specified album. You should not pass in the album id
867 * for the "unknown" album here (use -1 instead)
868 */
Marco Nelissenbd447b62009-06-29 14:52:05 -0700869 public static Bitmap getArtwork(Context context, long song_id, long album_id) {
The Android Open Source Project792a2202009-03-03 19:32:30 -0800870
871 if (album_id < 0) {
872 // This is something that is not in the database, so get the album art directly
873 // from the file.
Marco Nelissen7a16cc72009-06-16 08:54:17 -0700874 if (song_id >= 0) {
875 Bitmap bm = getArtworkFromFile(context, song_id, -1);
876 if (bm != null) {
877 return bm;
878 }
The Android Open Source Project792a2202009-03-03 19:32:30 -0800879 }
Marco Nelissen7a16cc72009-06-16 08:54:17 -0700880 return getDefaultArtwork(context);
The Android Open Source Project792a2202009-03-03 19:32:30 -0800881 }
882
883 ContentResolver res = context.getContentResolver();
884 Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id);
885 if (uri != null) {
886 InputStream in = null;
887 try {
888 in = res.openInputStream(uri);
889 return BitmapFactory.decodeStream(in, null, sBitmapOptions);
890 } catch (FileNotFoundException ex) {
891 // The album art thumbnail does not actually exist. Maybe the user deleted it, or
892 // maybe it never existed to begin with.
Marco Nelissen7a16cc72009-06-16 08:54:17 -0700893 Bitmap bm = getArtworkFromFile(context, song_id, album_id);
The Android Open Source Project792a2202009-03-03 19:32:30 -0800894 if (bm != null) {
Marco Nelissen756c3f52009-05-14 10:07:23 -0700895 if (bm.getConfig() == null) {
896 bm = bm.copy(Bitmap.Config.RGB_565, false);
897 if (bm == null) {
Marco Nelissen7a16cc72009-06-16 08:54:17 -0700898 return getDefaultArtwork(context);
Marco Nelissen756c3f52009-05-14 10:07:23 -0700899 }
900 }
The Android Open Source Project792a2202009-03-03 19:32:30 -0800901 } else {
Marco Nelissen7a16cc72009-06-16 08:54:17 -0700902 bm = getDefaultArtwork(context);
The Android Open Source Project792a2202009-03-03 19:32:30 -0800903 }
904 return bm;
905 } finally {
906 try {
907 if (in != null) {
908 in.close();
909 }
910 } catch (IOException ex) {
911 }
912 }
913 }
914
915 return null;
916 }
The Android Open Source Project792a2202009-03-03 19:32:30 -0800917
918 // get album art for specified file
919 private static final String sExternalMediaUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString();
Marco Nelissenbd447b62009-06-29 14:52:05 -0700920 private static Bitmap getArtworkFromFile(Context context, long songid, long albumid) {
The Android Open Source Project792a2202009-03-03 19:32:30 -0800921 Bitmap bm = null;
922 byte [] art = null;
923 String path = null;
924
Marco Nelissen7a16cc72009-06-16 08:54:17 -0700925 if (albumid < 0 && songid < 0) {
926 throw new IllegalArgumentException("Must specify an album or a song id");
The Android Open Source Project792a2202009-03-03 19:32:30 -0800927 }
Marco Nelissen7a16cc72009-06-16 08:54:17 -0700928
929 try {
930 if (albumid < 0) {
931 Uri uri = Uri.parse("content://media/external/audio/media/" + songid + "/albumart");
932 ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
933 if (pfd != null) {
934 FileDescriptor fd = pfd.getFileDescriptor();
935 bm = BitmapFactory.decodeFileDescriptor(fd);
936 }
937 } else {
938 Uri uri = ContentUris.withAppendedId(sArtworkUri, albumid);
939 ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
940 if (pfd != null) {
941 FileDescriptor fd = pfd.getFileDescriptor();
942 bm = BitmapFactory.decodeFileDescriptor(fd);
The Android Open Source Project792a2202009-03-03 19:32:30 -0800943 }
944 }
Marco Nelissen7a16cc72009-06-16 08:54:17 -0700945 } catch (FileNotFoundException ex) {
946 //
The Android Open Source Project792a2202009-03-03 19:32:30 -0800947 }
Marco Nelissen7a16cc72009-06-16 08:54:17 -0700948 if (bm != null) {
949 mCachedBit = bm;
The Android Open Source Project792a2202009-03-03 19:32:30 -0800950 }
951 return bm;
952 }
953
954 private static Bitmap getDefaultArtwork(Context context) {
955 BitmapFactory.Options opts = new BitmapFactory.Options();
956 opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
957 return BitmapFactory.decodeStream(
958 context.getResources().openRawResource(R.drawable.albumart_mp_unknown), null, opts);
959 }
960
961 static int getIntPref(Context context, String name, int def) {
962 SharedPreferences prefs =
963 context.getSharedPreferences("com.android.music", Context.MODE_PRIVATE);
964 return prefs.getInt(name, def);
965 }
966
967 static void setIntPref(Context context, String name, int value) {
968 SharedPreferences prefs =
969 context.getSharedPreferences("com.android.music", Context.MODE_PRIVATE);
970 Editor ed = prefs.edit();
971 ed.putInt(name, value);
972 ed.commit();
973 }
974
975 static void setRingtone(Context context, long id) {
976 ContentResolver resolver = context.getContentResolver();
977 // Set the flag in the database to mark this as a ringtone
978 Uri ringUri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
979 try {
980 ContentValues values = new ContentValues(2);
981 values.put(MediaStore.Audio.Media.IS_RINGTONE, "1");
982 values.put(MediaStore.Audio.Media.IS_ALARM, "1");
983 resolver.update(ringUri, values, null, null);
984 } catch (UnsupportedOperationException ex) {
985 // most likely the card just got unmounted
986 Log.e(TAG, "couldn't set ringtone flag for id " + id);
987 return;
988 }
989
990 String[] cols = new String[] {
991 MediaStore.Audio.Media._ID,
992 MediaStore.Audio.Media.DATA,
993 MediaStore.Audio.Media.TITLE
994 };
995
996 String where = MediaStore.Audio.Media._ID + "=" + id;
997 Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
998 cols, where , null, null);
999 try {
1000 if (cursor != null && cursor.getCount() == 1) {
1001 // Set the system setting to make this the current ringtone
1002 cursor.moveToFirst();
1003 Settings.System.putString(resolver, Settings.System.RINGTONE, ringUri.toString());
1004 String message = context.getString(R.string.ringtone_set, cursor.getString(2));
1005 Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
1006 }
1007 } finally {
1008 if (cursor != null) {
1009 cursor.close();
1010 }
1011 }
1012 }
1013}