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