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