1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.music; 18 19 import com.android.music.MusicUtils.ServiceToken; 20 21 import android.app.ListActivity; 22 import android.app.SearchManager; 23 import android.content.AsyncQueryHandler; 24 import android.content.BroadcastReceiver; 25 import android.content.ComponentName; 26 import android.content.ContentResolver; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.IntentFilter; 30 import android.content.ServiceConnection; 31 32 import android.database.Cursor; 33 import android.database.DatabaseUtils; 34 import android.media.AudioManager; 35 import android.net.Uri; 36 import android.os.Bundle; 37 import android.os.Handler; 38 import android.os.IBinder; 39 import android.os.Message; 40 import android.provider.BaseColumns; 41 import android.provider.MediaStore; 42 import android.text.TextUtils; 43 import android.util.Log; 44 import android.view.KeyEvent; 45 import android.view.MenuItem; 46 import android.view.View; 47 import android.view.ViewGroup; 48 import android.view.Window; 49 import android.view.ViewGroup.OnHierarchyChangeListener; 50 import android.widget.ImageView; 51 import android.widget.ListView; 52 import android.widget.SimpleCursorAdapter; 53 import android.widget.TextView; 54 55 import java.util.ArrayList; 56 57 public class QueryBrowserActivity 58 extends ListActivity implements MusicUtils.Defs, ServiceConnection { 59 private final static int PLAY_NOW = 0; 60 private final static int ADD_TO_QUEUE = 1; 61 private final static int PLAY_NEXT = 2; 62 private final static int PLAY_ARTIST = 3; 63 private final static int EXPLORE_ARTIST = 4; 64 private final static int PLAY_ALBUM = 5; 65 private final static int EXPLORE_ALBUM = 6; 66 private final static int REQUERY = 3; 67 private QueryListAdapter mAdapter; 68 private boolean mAdapterSent; 69 private String mFilterString = ""; 70 private ServiceToken mToken; 71 QueryBrowserActivity()72 public QueryBrowserActivity() {} 73 74 /** Called when the activity is first created. */ 75 @Override onCreate(Bundle icicle)76 public void onCreate(Bundle icicle) { 77 super.onCreate(icicle); 78 setVolumeControlStream(AudioManager.STREAM_MUSIC); 79 mAdapter = (QueryListAdapter) getLastNonConfigurationInstance(); 80 mToken = MusicUtils.bindToService(this, this); 81 // defer the real work until we're bound to the service 82 } 83 onServiceConnected(ComponentName name, IBinder service)84 public void onServiceConnected(ComponentName name, IBinder service) { 85 IntentFilter f = new IntentFilter(); 86 f.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED); 87 f.addAction(Intent.ACTION_MEDIA_UNMOUNTED); 88 f.addDataScheme("file"); 89 registerReceiver(mScanListener, f); 90 91 Intent intent = getIntent(); 92 String action = intent != null ? intent.getAction() : null; 93 94 if (Intent.ACTION_VIEW.equals(action)) { 95 // this is something we got from the search bar 96 Uri uri = intent.getData(); 97 String path = uri.toString(); 98 if (path.startsWith("content://media/external/audio/media/")) { 99 // This is a specific file 100 String id = uri.getLastPathSegment(); 101 long[] list = new long[] {Long.valueOf(id)}; 102 MusicUtils.playAll(this, list, 0); 103 finish(); 104 return; 105 } else if (path.startsWith("content://media/external/audio/albums/")) { 106 // This is an album, show the songs on it 107 Intent i = new Intent(Intent.ACTION_PICK); 108 i.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track"); 109 i.putExtra("album", uri.getLastPathSegment()); 110 startActivity(i); 111 finish(); 112 return; 113 } else if (path.startsWith("content://media/external/audio/artists/")) { 114 // This is an artist, show the albums for that artist 115 Intent i = new Intent(Intent.ACTION_PICK); 116 i.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/album"); 117 i.putExtra("artist", uri.getLastPathSegment()); 118 startActivity(i); 119 finish(); 120 return; 121 } 122 } 123 124 mFilterString = intent.getStringExtra(SearchManager.QUERY); 125 if (MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action)) { 126 String focus = intent.getStringExtra(MediaStore.EXTRA_MEDIA_FOCUS); 127 String artist = intent.getStringExtra(MediaStore.EXTRA_MEDIA_ARTIST); 128 String album = intent.getStringExtra(MediaStore.EXTRA_MEDIA_ALBUM); 129 String title = intent.getStringExtra(MediaStore.EXTRA_MEDIA_TITLE); 130 if (focus != null) { 131 if (focus.startsWith("audio/") && title != null) { 132 mFilterString = title; 133 } else if (focus.equals(MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE)) { 134 if (album != null) { 135 mFilterString = album; 136 if (artist != null) { 137 mFilterString = mFilterString + " " + artist; 138 } 139 } 140 } else if (focus.equals(MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE)) { 141 if (artist != null) { 142 mFilterString = artist; 143 } 144 } 145 } 146 } 147 148 setContentView(R.layout.query_activity); 149 mTrackList = getListView(); 150 mTrackList.setTextFilterEnabled(true); 151 if (mAdapter == null) { 152 mAdapter = new QueryListAdapter(getApplication(), this, R.layout.track_list_item, 153 null, // cursor 154 new String[] {}, new int[] {}); 155 setListAdapter(mAdapter); 156 if (TextUtils.isEmpty(mFilterString)) { 157 getQueryCursor(mAdapter.getQueryHandler(), null); 158 } else { 159 mTrackList.setFilterText(mFilterString); 160 mFilterString = null; 161 } 162 } else { 163 mAdapter.setActivity(this); 164 setListAdapter(mAdapter); 165 mQueryCursor = mAdapter.getCursor(); 166 if (mQueryCursor != null) { 167 init(mQueryCursor); 168 } else { 169 getQueryCursor(mAdapter.getQueryHandler(), mFilterString); 170 } 171 } 172 } 173 onServiceDisconnected(ComponentName name)174 public void onServiceDisconnected(ComponentName name) {} 175 176 @Override onRetainNonConfigurationInstance()177 public Object onRetainNonConfigurationInstance() { 178 mAdapterSent = true; 179 return mAdapter; 180 } 181 182 @Override onPause()183 public void onPause() { 184 mReScanHandler.removeCallbacksAndMessages(null); 185 super.onPause(); 186 } 187 188 @Override onDestroy()189 public void onDestroy() { 190 MusicUtils.unbindFromService(mToken); 191 unregisterReceiver(mScanListener); 192 // If we have an adapter and didn't send it off to another activity yet, we should 193 // close its cursor, which we do by assigning a null cursor to it. Doing this 194 // instead of closing the cursor directly keeps the framework from accessing 195 // the closed cursor later. 196 if (!mAdapterSent && mAdapter != null) { 197 mAdapter.changeCursor(null); 198 } 199 // Because we pass the adapter to the next activity, we need to make 200 // sure it doesn't keep a reference to this activity. We can do this 201 // by clearing its DatasetObservers, which setListAdapter(null) does. 202 if (getListView() != null) { 203 setListAdapter(null); 204 } 205 mAdapter = null; 206 super.onDestroy(); 207 } 208 209 /* 210 * This listener gets called when the media scanner starts up, and when the 211 * sd card is unmounted. 212 */ 213 private BroadcastReceiver mScanListener = new BroadcastReceiver() { 214 @Override 215 public void onReceive(Context context, Intent intent) { 216 MusicUtils.setSpinnerState(QueryBrowserActivity.this); 217 mReScanHandler.sendEmptyMessage(0); 218 } 219 }; 220 221 private Handler mReScanHandler = new Handler() { 222 @Override 223 public void handleMessage(Message msg) { 224 if (mAdapter != null) { 225 getQueryCursor(mAdapter.getQueryHandler(), null); 226 } 227 // if the query results in a null cursor, onQueryComplete() will 228 // call init(), which will post a delayed message to this handler 229 // in order to try again. 230 } 231 }; 232 233 @Override onActivityResult(int requestCode, int resultCode, Intent intent)234 protected void onActivityResult(int requestCode, int resultCode, Intent intent) { 235 switch (requestCode) { 236 case SCAN_DONE: 237 if (resultCode == RESULT_CANCELED) { 238 finish(); 239 } else { 240 getQueryCursor(mAdapter.getQueryHandler(), null); 241 } 242 break; 243 } 244 } 245 init(Cursor c)246 public void init(Cursor c) { 247 if (mAdapter == null) { 248 return; 249 } 250 mAdapter.changeCursor(c); 251 252 if (mQueryCursor == null) { 253 MusicUtils.displayDatabaseError(this); 254 setListAdapter(null); 255 mReScanHandler.sendEmptyMessageDelayed(0, 1000); 256 return; 257 } 258 MusicUtils.hideDatabaseError(this); 259 } 260 261 @Override onListItemClick(ListView l, View v, int position, long id)262 protected void onListItemClick(ListView l, View v, int position, long id) { 263 // Dialog doesn't allow us to wait for a result, so we need to store 264 // the info we need for when the dialog posts its result 265 mQueryCursor.moveToPosition(position); 266 if (mQueryCursor.isBeforeFirst() || mQueryCursor.isAfterLast()) { 267 return; 268 } 269 String selectedType = mQueryCursor.getString( 270 mQueryCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.MIME_TYPE)); 271 272 if ("artist".equals(selectedType)) { 273 Intent intent = new Intent(Intent.ACTION_PICK); 274 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 275 intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/album"); 276 intent.putExtra("artist", Long.valueOf(id).toString()); 277 startActivity(intent); 278 } else if ("album".equals(selectedType)) { 279 Intent intent = new Intent(Intent.ACTION_PICK); 280 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 281 intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track"); 282 intent.putExtra("album", Long.valueOf(id).toString()); 283 startActivity(intent); 284 } else if (position >= 0 && id >= 0) { 285 long[] list = new long[] {id}; 286 MusicUtils.playAll(this, list, 0); 287 } else { 288 Log.e("QueryBrowser", "invalid position/id: " + position + "/" + id); 289 } 290 } 291 292 @Override onOptionsItemSelected(MenuItem item)293 public boolean onOptionsItemSelected(MenuItem item) { 294 switch (item.getItemId()) { 295 case USE_AS_RINGTONE: { 296 // Set the system setting to make this the current ringtone 297 MusicUtils.setRingtone(this, mTrackList.getSelectedItemId()); 298 return true; 299 } 300 } 301 return super.onOptionsItemSelected(item); 302 } 303 getQueryCursor(AsyncQueryHandler async, String filter)304 private Cursor getQueryCursor(AsyncQueryHandler async, String filter) { 305 if (filter == null) { 306 filter = ""; 307 } 308 String[] ccols = new String[] { 309 BaseColumns._ID, // this will be the artist, album or track ID 310 MediaStore.Audio.Media.MIME_TYPE, // mimetype of audio file, or "artist" or "album" 311 MediaStore.Audio.Artists.ARTIST, MediaStore.Audio.Albums.ALBUM, 312 MediaStore.Audio.Media.TITLE, "data1", "data2"}; 313 314 Uri search = Uri.parse("content://media/external/audio/search/fancy/" + Uri.encode(filter)); 315 316 Cursor ret = null; 317 if (async != null) { 318 async.startQuery(0, null, search, ccols, null, null, null); 319 } else { 320 ret = MusicUtils.query(this, search, ccols, null, null, null); 321 } 322 return ret; 323 } 324 325 static class QueryListAdapter extends SimpleCursorAdapter { 326 private QueryBrowserActivity mActivity = null; 327 private AsyncQueryHandler mQueryHandler; 328 private String mConstraint = null; 329 private boolean mConstraintIsValid = false; 330 331 class QueryHandler extends AsyncQueryHandler { QueryHandler(ContentResolver res)332 QueryHandler(ContentResolver res) { 333 super(res); 334 } 335 336 @Override onQueryComplete(int token, Object cookie, Cursor cursor)337 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 338 mActivity.init(cursor); 339 } 340 } 341 QueryListAdapter(Context context, QueryBrowserActivity currentactivity, int layout, Cursor cursor, String[] from, int[] to)342 QueryListAdapter(Context context, QueryBrowserActivity currentactivity, int layout, 343 Cursor cursor, String[] from, int[] to) { 344 super(context, layout, cursor, from, to); 345 mActivity = currentactivity; 346 mQueryHandler = new QueryHandler(context.getContentResolver()); 347 } 348 setActivity(QueryBrowserActivity newactivity)349 public void setActivity(QueryBrowserActivity newactivity) { 350 mActivity = newactivity; 351 } 352 getQueryHandler()353 public AsyncQueryHandler getQueryHandler() { 354 return mQueryHandler; 355 } 356 357 @Override bindView(View view, Context context, Cursor cursor)358 public void bindView(View view, Context context, Cursor cursor) { 359 TextView tv1 = (TextView) view.findViewById(R.id.line1); 360 TextView tv2 = (TextView) view.findViewById(R.id.line2); 361 ImageView iv = (ImageView) view.findViewById(R.id.icon); 362 ViewGroup.LayoutParams p = iv.getLayoutParams(); 363 if (p == null) { 364 // seen this happen, not sure why 365 DatabaseUtils.dumpCursor(cursor); 366 return; 367 } 368 p.width = ViewGroup.LayoutParams.WRAP_CONTENT; 369 p.height = ViewGroup.LayoutParams.WRAP_CONTENT; 370 371 String mimetype = cursor.getString( 372 cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.MIME_TYPE)); 373 374 if (mimetype == null) { 375 mimetype = "audio/"; 376 } 377 if (mimetype.equals("artist")) { 378 iv.setImageResource(R.drawable.ic_mp_artist_list); 379 String name = cursor.getString( 380 cursor.getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST)); 381 String displayname = name; 382 boolean isunknown = false; 383 if (name == null || name.equals(MediaStore.UNKNOWN_STRING)) { 384 displayname = context.getString(R.string.unknown_artist_name); 385 isunknown = true; 386 } 387 tv1.setText(displayname); 388 389 int numalbums = cursor.getInt(cursor.getColumnIndexOrThrow("data1")); 390 int numsongs = cursor.getInt(cursor.getColumnIndexOrThrow("data2")); 391 392 String songs_albums = 393 MusicUtils.makeAlbumsSongsLabel(context, numalbums, numsongs, isunknown); 394 395 tv2.setText(songs_albums); 396 397 } else if (mimetype.equals("album")) { 398 iv.setImageResource(R.drawable.albumart_mp_unknown_list); 399 String name = cursor.getString( 400 cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM)); 401 String displayname = name; 402 if (name == null || name.equals(MediaStore.UNKNOWN_STRING)) { 403 displayname = context.getString(R.string.unknown_album_name); 404 } 405 tv1.setText(displayname); 406 407 name = cursor.getString( 408 cursor.getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST)); 409 displayname = name; 410 if (name == null || name.equals(MediaStore.UNKNOWN_STRING)) { 411 displayname = context.getString(R.string.unknown_artist_name); 412 } 413 tv2.setText(displayname); 414 415 } else if (mimetype.startsWith("audio/") || mimetype.equals("application/ogg") 416 || mimetype.equals("application/x-ogg")) { 417 iv.setImageResource(R.drawable.ic_mp_song_list); 418 String name = cursor.getString( 419 cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)); 420 tv1.setText(name); 421 422 String displayname = cursor.getString( 423 cursor.getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST)); 424 if (displayname == null || displayname.equals(MediaStore.UNKNOWN_STRING)) { 425 displayname = context.getString(R.string.unknown_artist_name); 426 } 427 name = cursor.getString( 428 cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM)); 429 if (name == null || name.equals(MediaStore.UNKNOWN_STRING)) { 430 name = context.getString(R.string.unknown_album_name); 431 } 432 tv2.setText(displayname + " - " + name); 433 } 434 } 435 @Override changeCursor(Cursor cursor)436 public void changeCursor(Cursor cursor) { 437 if (mActivity.isFinishing() && cursor != null) { 438 cursor.close(); 439 cursor = null; 440 } 441 if (cursor != mActivity.mQueryCursor) { 442 mActivity.mQueryCursor = cursor; 443 super.changeCursor(cursor); 444 } 445 } 446 @Override runQueryOnBackgroundThread(CharSequence constraint)447 public Cursor runQueryOnBackgroundThread(CharSequence constraint) { 448 String s = constraint.toString(); 449 if (mConstraintIsValid && ((s == null && mConstraint == null) 450 || (s != null && s.equals(mConstraint)))) { 451 return getCursor(); 452 } 453 Cursor c = mActivity.getQueryCursor(null, s); 454 mConstraint = s; 455 mConstraintIsValid = true; 456 return c; 457 } 458 } 459 460 private ListView mTrackList; 461 private Cursor mQueryCursor; 462 } 463