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 if (getListView() != null) { 214 setListAdapter(null); 215 } 216 mAdapter = null; 217 super.onDestroy(); 218 } 219 220 /* 221 * This listener gets called when the media scanner starts up, and when the 222 * sd card is unmounted. 223 */ 224 private BroadcastReceiver mScanListener = new BroadcastReceiver() { 225 @Override 226 public void onReceive(Context context, Intent intent) { 227 MusicUtils.setSpinnerState(QueryBrowserActivity.this); 228 mReScanHandler.sendEmptyMessage(0); 229 } 230 }; 231 232 private Handler mReScanHandler = new Handler() { 233 @Override 234 public void handleMessage(Message msg) { 235 if (mAdapter != null) { 236 getQueryCursor(mAdapter.getQueryHandler(), null); 237 } 238 // if the query results in a null cursor, onQueryComplete() will 239 // call init(), which will post a delayed message to this handler 240 // in order to try again. 241 } 242 }; 243 244 @Override onActivityResult(int requestCode, int resultCode, Intent intent)245 protected void onActivityResult(int requestCode, int resultCode, Intent intent) { 246 switch (requestCode) { 247 case SCAN_DONE: 248 if (resultCode == RESULT_CANCELED) { 249 finish(); 250 } else { 251 getQueryCursor(mAdapter.getQueryHandler(), null); 252 } 253 break; 254 } 255 } 256 init(Cursor c)257 public void init(Cursor c) { 258 259 if (mAdapter == null) { 260 return; 261 } 262 mAdapter.changeCursor(c); 263 264 if (mQueryCursor == null) { 265 MusicUtils.displayDatabaseError(this); 266 setListAdapter(null); 267 mReScanHandler.sendEmptyMessageDelayed(0, 1000); 268 return; 269 } 270 MusicUtils.hideDatabaseError(this); 271 } 272 273 @Override onListItemClick(ListView l, View v, int position, long id)274 protected void onListItemClick(ListView l, View v, int position, long id) 275 { 276 // Dialog doesn't allow us to wait for a result, so we need to store 277 // the info we need for when the dialog posts its result 278 mQueryCursor.moveToPosition(position); 279 if (mQueryCursor.isBeforeFirst() || mQueryCursor.isAfterLast()) { 280 return; 281 } 282 String selectedType = mQueryCursor.getString(mQueryCursor.getColumnIndexOrThrow( 283 MediaStore.Audio.Media.MIME_TYPE)); 284 285 if ("artist".equals(selectedType)) { 286 Intent intent = new Intent(Intent.ACTION_PICK); 287 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 288 intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/album"); 289 intent.putExtra("artist", Long.valueOf(id).toString()); 290 startActivity(intent); 291 } else if ("album".equals(selectedType)) { 292 Intent intent = new Intent(Intent.ACTION_PICK); 293 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 294 intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track"); 295 intent.putExtra("album", Long.valueOf(id).toString()); 296 startActivity(intent); 297 } else if (position >= 0 && id >= 0){ 298 long [] list = new long[] { id }; 299 MusicUtils.playAll(this, list, 0); 300 } else { 301 Log.e("QueryBrowser", "invalid position/id: " + position + "/" + id); 302 } 303 } 304 305 @Override onOptionsItemSelected(MenuItem item)306 public boolean onOptionsItemSelected(MenuItem item) { 307 switch (item.getItemId()) { 308 case USE_AS_RINGTONE: { 309 // Set the system setting to make this the current ringtone 310 MusicUtils.setRingtone(this, mTrackList.getSelectedItemId()); 311 return true; 312 } 313 314 } 315 return super.onOptionsItemSelected(item); 316 } 317 getQueryCursor(AsyncQueryHandler async, String filter)318 private Cursor getQueryCursor(AsyncQueryHandler async, String filter) { 319 if (filter == null) { 320 filter = ""; 321 } 322 String[] ccols = new String[] { 323 BaseColumns._ID, // this will be the artist, album or track ID 324 MediaStore.Audio.Media.MIME_TYPE, // mimetype of audio file, or "artist" or "album" 325 MediaStore.Audio.Artists.ARTIST, 326 MediaStore.Audio.Albums.ALBUM, 327 MediaStore.Audio.Media.TITLE, 328 "data1", 329 "data2" 330 }; 331 332 Uri search = Uri.parse("content://media/external/audio/search/fancy/" + 333 Uri.encode(filter)); 334 335 Cursor ret = null; 336 if (async != null) { 337 async.startQuery(0, null, search, ccols, null, null, null); 338 } else { 339 ret = MusicUtils.query(this, search, ccols, null, null, null); 340 } 341 return ret; 342 } 343 344 static class QueryListAdapter extends SimpleCursorAdapter { 345 private QueryBrowserActivity mActivity = null; 346 private AsyncQueryHandler mQueryHandler; 347 private String mConstraint = null; 348 private boolean mConstraintIsValid = false; 349 350 class QueryHandler extends AsyncQueryHandler { QueryHandler(ContentResolver res)351 QueryHandler(ContentResolver res) { 352 super(res); 353 } 354 355 @Override onQueryComplete(int token, Object cookie, Cursor cursor)356 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 357 mActivity.init(cursor); 358 } 359 } 360 QueryListAdapter(Context context, QueryBrowserActivity currentactivity, int layout, Cursor cursor, String[] from, int[] to)361 QueryListAdapter(Context context, QueryBrowserActivity currentactivity, 362 int layout, Cursor cursor, String[] from, int[] to) { 363 super(context, layout, cursor, from, to); 364 mActivity = currentactivity; 365 mQueryHandler = new QueryHandler(context.getContentResolver()); 366 } 367 setActivity(QueryBrowserActivity newactivity)368 public void setActivity(QueryBrowserActivity newactivity) { 369 mActivity = newactivity; 370 } 371 getQueryHandler()372 public AsyncQueryHandler getQueryHandler() { 373 return mQueryHandler; 374 } 375 376 @Override bindView(View view, Context context, Cursor cursor)377 public void bindView(View view, Context context, Cursor cursor) { 378 379 TextView tv1 = (TextView) view.findViewById(R.id.line1); 380 TextView tv2 = (TextView) view.findViewById(R.id.line2); 381 ImageView iv = (ImageView) view.findViewById(R.id.icon); 382 ViewGroup.LayoutParams p = iv.getLayoutParams(); 383 if (p == null) { 384 // seen this happen, not sure why 385 DatabaseUtils.dumpCursor(cursor); 386 return; 387 } 388 p.width = ViewGroup.LayoutParams.WRAP_CONTENT; 389 p.height = ViewGroup.LayoutParams.WRAP_CONTENT; 390 391 String mimetype = cursor.getString(cursor.getColumnIndexOrThrow( 392 MediaStore.Audio.Media.MIME_TYPE)); 393 394 if (mimetype == null) { 395 mimetype = "audio/"; 396 } 397 if (mimetype.equals("artist")) { 398 iv.setImageResource(R.drawable.ic_mp_artist_list); 399 String name = cursor.getString(cursor.getColumnIndexOrThrow( 400 MediaStore.Audio.Artists.ARTIST)); 401 String displayname = name; 402 boolean isunknown = false; 403 if (name == null || name.equals(MediaStore.UNKNOWN_STRING)) { 404 displayname = context.getString(R.string.unknown_artist_name); 405 isunknown = true; 406 } 407 tv1.setText(displayname); 408 409 int numalbums = cursor.getInt(cursor.getColumnIndexOrThrow("data1")); 410 int numsongs = cursor.getInt(cursor.getColumnIndexOrThrow("data2")); 411 412 String songs_albums = MusicUtils.makeAlbumsSongsLabel(context, 413 numalbums, numsongs, isunknown); 414 415 tv2.setText(songs_albums); 416 417 } else if (mimetype.equals("album")) { 418 iv.setImageResource(R.drawable.albumart_mp_unknown_list); 419 String name = cursor.getString(cursor.getColumnIndexOrThrow( 420 MediaStore.Audio.Albums.ALBUM)); 421 String displayname = name; 422 if (name == null || name.equals(MediaStore.UNKNOWN_STRING)) { 423 displayname = context.getString(R.string.unknown_album_name); 424 } 425 tv1.setText(displayname); 426 427 name = cursor.getString(cursor.getColumnIndexOrThrow( 428 MediaStore.Audio.Artists.ARTIST)); 429 displayname = name; 430 if (name == null || name.equals(MediaStore.UNKNOWN_STRING)) { 431 displayname = context.getString(R.string.unknown_artist_name); 432 } 433 tv2.setText(displayname); 434 435 } else if(mimetype.startsWith("audio/") || 436 mimetype.equals("application/ogg") || 437 mimetype.equals("application/x-ogg")) { 438 iv.setImageResource(R.drawable.ic_mp_song_list); 439 String name = cursor.getString(cursor.getColumnIndexOrThrow( 440 MediaStore.Audio.Media.TITLE)); 441 tv1.setText(name); 442 443 String displayname = cursor.getString(cursor.getColumnIndexOrThrow( 444 MediaStore.Audio.Artists.ARTIST)); 445 if (displayname == null || displayname.equals(MediaStore.UNKNOWN_STRING)) { 446 displayname = context.getString(R.string.unknown_artist_name); 447 } 448 name = cursor.getString(cursor.getColumnIndexOrThrow( 449 MediaStore.Audio.Albums.ALBUM)); 450 if (name == null || name.equals(MediaStore.UNKNOWN_STRING)) { 451 name = context.getString(R.string.unknown_album_name); 452 } 453 tv2.setText(displayname + " - " + name); 454 } 455 } 456 @Override changeCursor(Cursor cursor)457 public void changeCursor(Cursor cursor) { 458 if (mActivity.isFinishing() && cursor != null) { 459 cursor.close(); 460 cursor = null; 461 } 462 if (cursor != mActivity.mQueryCursor) { 463 mActivity.mQueryCursor = cursor; 464 super.changeCursor(cursor); 465 } 466 } 467 @Override runQueryOnBackgroundThread(CharSequence constraint)468 public Cursor runQueryOnBackgroundThread(CharSequence constraint) { 469 String s = constraint.toString(); 470 if (mConstraintIsValid && ( 471 (s == null && mConstraint == null) || 472 (s != null && s.equals(mConstraint)))) { 473 return getCursor(); 474 } 475 Cursor c = mActivity.getQueryCursor(null, s); 476 mConstraint = s; 477 mConstraintIsValid = true; 478 return c; 479 } 480 } 481 482 private ListView mTrackList; 483 private Cursor mQueryCursor; 484 } 485 486