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