Java tutorial
/* * ShowDetailsScreen.java * VERSION 2.0 * * Copyright 2011 Andrew Pearson and Sanders DeNardi. * * This file is part of Vibe Vault. * * Vibe Vault is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * */ package com.code.android.vibevault; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.List; import org.apache.http.util.ByteArrayBuffer; import org.htmlcleaner.CleanerProperties; import org.htmlcleaner.HtmlCleaner; import org.htmlcleaner.TagNode; import org.htmlcleaner.XPatherException; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.ProgressDialog; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.IBinder; import android.util.Log; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.View.OnCreateContextMenuListener; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.code.android.vibevault.R; public class ShowDetailsScreen extends Activity { private static final String LOG_TAG = ShowDetailsScreen.class.getName(); private PlaybackService pService; private DownloadService dService = null; private ArrayList<ArchiveSongObj> downloadLinks; private ArchiveShowObj show; private TextView showLabel; private ListView trackList; private ParseShowDetailsPageTask workerTask; private boolean dialogShown; private String showTitle; // This is set to -1 UNLESS ShowDetailsScreen is being opened by an intent from clicking // on a song (not a show link). it is set in the AsyncTask which parses the show // as the index of the song that the user clicked on. private int selectedPos = -1; /** Create the activity, taking into account ongoing dialogs or already downloaded data. * * If there is a retained ParseShowDetailsPageTask, set its parent * activity to the newly created ShowDetailsScreen (the old one was * destroyed because of an orientation change or something. This * way, the ParseShowDetailsPageTask does not leak any of the Views * from the old ShowDetailsScreen. Also, grab the songs from the * ParseShowDetailsPageTask to refresh the list of songs with. */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.show_details_screen); Intent intent = getIntent(); Bundle b = intent.getExtras(); if (b != null) { show = (ArchiveShowObj) b.get("Show"); } if (show == null) { if (intent.getScheme().equals("http")) { Uri link = intent.getData(); String linkString = link.toString(); if (linkString.contains("/download/")) { String[] paths = linkString.split("/"); for (int i = 0; i < paths.length; i++) { if (paths[i].equals("download")) { show = new ArchiveShowObj(Uri.parse("http://www.archive.org/details/" + paths[i + 1]), true); show.setSelectedSong(linkString); } } } else { show = new ArchiveShowObj(link, false); } } } // // showTitle = show.getArtistAndTitle(); showLabel = (TextView) findViewById(R.id.ShowLabel); showLabel.setText(showTitle); trackList = (ListView) findViewById(R.id.SongsListView); trackList.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> a, View v, int position, long id) { playShow(position); Intent i = new Intent(ShowDetailsScreen.this, NowPlayingScreen.class); startActivity(i); } }); trackList.setOnCreateContextMenuListener(new OnCreateContextMenuListener() { @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { menu.add(Menu.NONE, VibeVault.ADD_SONG_TO_QUEUE, Menu.NONE, "Add to playlist"); menu.add(Menu.NONE, VibeVault.DOWNLOAD_SONG, Menu.NONE, "Download Song"); menu.add(Menu.NONE, VibeVault.EMAIL_LINK, Menu.NONE, "Email Link to Song"); } }); downloadLinks = new ArrayList<ArchiveSongObj>(); Object retained = getLastNonConfigurationInstance(); if (retained instanceof ParseShowDetailsPageTask) { workerTask = (ParseShowDetailsPageTask) retained; workerTask.setActivity(this); downloadLinks = workerTask.songs; } else if (show.getShowURL() != null) { workerTask = new ParseShowDetailsPageTask(this); workerTask.execute(show); } } private void refreshScreenTitle() { showTitle = show.getArtistAndTitle(); showLabel = (TextView) findViewById(R.id.ShowLabel); showLabel.setText(showTitle); } @Override public void onResume() { super.onResume(); getApplicationContext().bindService(new Intent(this, DownloadService.class), onDService, BIND_AUTO_CREATE); attachToPlaybackService(); registerReceiver(TitleReceiver, new IntentFilter(VibeVault.BROADCAST_SONG_TITLE)); refreshTrackList(); } @Override public void onPause() { super.onPause(); getApplicationContext().unbindService(onDService); detachFromPlaybackService(); unregisterReceiver(TitleReceiver); } private void attachToPlaybackService() { Intent serviceIntent = new Intent(this, PlaybackService.class); // Explicitly start the service. Don't use BIND_AUTO_CREATE, since it // causes an implicit service stop when the last binder is removed. getApplicationContext().startService(serviceIntent); getApplicationContext().bindService(serviceIntent, conn, 0); } private void detachFromPlaybackService() { getApplicationContext().unbindService(conn); } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.help_recentshows_nowplaying_email_options, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.nowPlaying: //Open playlist activity Intent i = new Intent(ShowDetailsScreen.this, NowPlayingScreen.class); startActivity(i); break; case R.id.recentShows: Intent rs = new Intent(ShowDetailsScreen.this, RecentShowsScreen.class); startActivity(rs); break; case R.id.scrollableDialog: AlertDialog.Builder ad = new AlertDialog.Builder(this); ad.setTitle("Help!"); View v = LayoutInflater.from(this).inflate(R.layout.scrollable_dialog, null); ((TextView) v.findViewById(R.id.DialogText)).setText(R.string.show_details_screen_help); ad.setPositiveButton("Okay.", new android.content.DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int arg1) { } }); ad.setView(v); ad.show(); break; case R.id.emailLink: final Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND); emailIntent.setType("plain/text"); emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, "Great show on archive.org: " + show.getArtistAndTitle()); emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, "Hey,\n\nYou should listen to " + show.getArtistAndTitle() + ". You can find it here: " + show.getShowURL() + "\n\nSent using VibeVault for Android."); startActivity(Intent.createChooser(emailIntent, "Send mail...")); break; case R.id.downloadShow: for (int j = 0; j < downloadLinks.size(); j++) { downloadLinks.get(j).setDownloadShow(show); dService.addSong(downloadLinks.get(j)); } break; default: break; } return true; } private void playShow(int pos) { if (!downloadLinks.isEmpty()) { VibeVault.playList.setPlayList(downloadLinks); pService.playSongFromPlaylist(pos); } } /** Handle user's long-click selection. * */ @Override public boolean onContextItemSelected(MenuItem item) { AdapterContextMenuInfo menuInfo = (AdapterContextMenuInfo) item.getMenuInfo(); if (menuInfo != null) { ArchiveSongObj selSong = (ArchiveSongObj) trackList.getAdapter().getItem(menuInfo.position); switch (item.getItemId()) { case (VibeVault.STREAM_CONTEXT_ID): // Start streaming. int track = pService.enqueue(selSong); pService.playSongFromPlaylist(track); break; case (VibeVault.DOWNLOAD_SONG): selSong.setDownloadShow(show); dService.addSong(selSong); break; case (VibeVault.ADD_SONG_TO_QUEUE): pService.enqueue(selSong); break; case (VibeVault.EMAIL_LINK): final Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND); emailIntent.setType("plain/text"); emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, "Great song on archive.org: " + selSong.toString()); emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, "Hey,\n\nI found a song you should listen to. It's called " + selSong.toString() + " and it's off of " + selSong.getShowTitle() + ". You can get it here: " + selSong.getLowBitRate() + "\n\nSent using VibeVault for Android."); startActivity(Intent.createChooser(emailIntent, "Send mail...")); break; default: return false; } return true; } return false; } /** Persist worker Thread across orientation changes. * * Includes Thread bookkeeping to prevent not leaking Views on orientation changes. */ @Override public Object onRetainNonConfigurationInstance() { workerTask.setActivity(null); return workerTask; } /** Dialog preparation method. * * Includes Thread bookkeeping to prevent not leaking Views on orientation changes. */ @Override protected void onPrepareDialog(int id, Dialog dialog) { super.onPrepareDialog(id, dialog); if (id == VibeVault.LOADING_DIALOG_ID) { dialogShown = true; } } /** Dialog creation method. * * Includes Thread bookkeeping to prevent not leaking Views on orientation changes. */ @Override protected Dialog onCreateDialog(int id) { switch (id) { case VibeVault.LOADING_DIALOG_ID: ProgressDialog dialog = new ProgressDialog(this); dialog.setMessage("Loading"); return dialog; default: return super.onCreateDialog(id); } } /** Refresh the track list with whatever data List of songs contains. * */ private void refreshTrackList() { trackList.setAdapter(new ColoredAdapter(this, R.layout.show_details_screen_row, downloadLinks)); } /** Bookkeeping method to deal with dialogs over orientation changes. * */ private void onTaskCompleted() { if (dialogShown) { try { dismissDialog(VibeVault.LOADING_DIALOG_ID); } catch (IllegalArgumentException e) { e.printStackTrace(); } dialogShown = false; refreshScreenTitle(); refreshTrackList(); // If this is from an Intent where a song was selected, play it. if (selectedPos != -1) { playShow(selectedPos); Intent i = new Intent(ShowDetailsScreen.this, NowPlayingScreen.class); startActivity(i); } } } /** Update the List of songs, and tell the user if no songs are in the list. * */ private void setSongs(ArrayList<ArchiveSongObj> songs) { if (songs.size() == 0) { Toast.makeText(getBaseContext(), "Looks like the show's got no downloadable content... Maybe try again later...", Toast.LENGTH_SHORT).show(); } downloadLinks = songs; refreshTrackList(); } /** Parse the show details page. * * I didn't want to use anexternal .JAR to parse the HTML, but * it is better practice than "rolling my own" parser using regex's * or something. We use HtmlCleaner instead of TagSoup because * it is smaller, even though HtmlCleaner doesn't support 100% of * XPath features. We use another AsyncTask to not block the UI thread. * I don't know if this is really necessary, but I think that it is good * idea in case we want to use this Activity in different ways in the future. */ private class ParseShowDetailsPageTask extends AsyncTask<ArchiveShowObj, String, Void> { private ShowDetailsScreen parentScreen; private boolean completed; private ArrayList<ArchiveSongObj> songs = new ArrayList<ArchiveSongObj>(); private ArchiveShowObj taskShow; private ParseShowDetailsPageTask(ShowDetailsScreen activity) { this.parentScreen = activity; } @Override protected void onPreExecute() { parentScreen.showDialog(VibeVault.LOADING_DIALOG_ID); } @Override protected void onProgressUpdate(String... values) { Toast.makeText(getBaseContext(), values[0], Toast.LENGTH_SHORT).show(); } @Override protected Void doInBackground(ArchiveShowObj... show) { taskShow = show[0]; HtmlCleaner pageParser = new HtmlCleaner(); CleanerProperties props = pageParser.getProperties(); props.setAllowHtmlInsideAttributes(true); props.setAllowMultiWordAttributes(true); props.setRecognizeUnicodeChars(true); props.setOmitComments(true); ArrayList<String> songLinks = new ArrayList<String>(); // XPATH says "Select out of all 'table' elements with attribute 'class' // equal to 'fileFormats' which contain element 'tr'..." // String songXPath = "//table[@class='fileFormats']//tr"; String m3uXPath = "//script[@type='text/javascript']"; try { // Get the show's title, and create a TagNode of The page. URLConnection conn = show[0].getShowURL().openConnection(); String showTitle = show[0].getArtistAndTitle(); String showIdent = show[0].getIdentifier(); InputStreamReader is = new InputStreamReader(conn.getInputStream()); TagNode node = pageParser.clean(is); is.close(); URL m3uURL = null; if (VibeVault.db.getPref("downloadFormat").equalsIgnoreCase("LBR")) { if (show[0].hasLBR()) { m3uURL = new URL(show[0].getLinkPrefix() + "_64kb.m3u"); } else if (show[0].hasVBR()) { m3uURL = new URL(show[0].getLinkPrefix() + "_vbr.m3u"); this.publishProgress("Show has no low bitrate stream... Reverting to VBR."); } } else { if (show[0].hasVBR()) { m3uURL = new URL(show[0].getLinkPrefix() + "_vbr.m3u"); } else if (show[0].hasLBR()) { m3uURL = new URL(show[0].getLinkPrefix() + "_64kb.m3u"); this.publishProgress("Show has no VBR stream... Reverting to low bitrate stream."); } } if (m3uURL != null) { } else { m3uURL = new URL(show[0].getLinkPrefix() + "_vbr.m3u"); } if (m3uURL != null) { // Grab the M3U stream... URLConnection m3uConn = m3uURL.openConnection(); if (m3uConn == null) { } InputStream inStream = m3uConn.getInputStream(); BufferedInputStream bis = new BufferedInputStream(inStream); ByteArrayBuffer baf = new ByteArrayBuffer(50); int read = 0; int bufSize = 512; byte[] buffer = new byte[bufSize]; while (bis.available() == 0) { bis.close(); inStream.close(); m3uConn = m3uURL.openConnection(); inStream = m3uConn.getInputStream(); bis = new BufferedInputStream(inStream); } while (true) { read = bis.read(buffer); if (read == -1) { break; } baf.append(buffer, 0, read); } String m3uString = new String(baf.toByteArray()); bis.close(); inStream.close(); // Now split the .M3U file based on newlines. This will give us // the download links, which we store.. String m3uLinks[] = m3uString.split("\n"); for (String link : m3uLinks) { songLinks.add(link); } // Now use an XPATH evaluation to find all of the javascript scripts on the page. // If one of them can be split by "IAD.playlists = ", it should have the track names // in it. The second half of the split is valid javascript and can be interpreted, // therefore, as JSON. Pull the song titles out of that, and together with the download // links make ArchiveSongObjs and add them to the list of songs. Then, return, because // you don't need to try the other method of song aggregation. Object[] titleNodes = node.evaluateXPath(m3uXPath); for (Object titleNode : titleNodes) { String jsonString = ((TagNode) titleNode).getChildren().toString(); String jsonArray[] = jsonString.split("IAD.playlists = "); if ((jsonArray.length) > 1) { try { JSONObject jObject = new JSONObject(jsonArray[1]); if (taskShow.getShowArtist().equals("")) { parentScreen.show.setFullTitle(jObject.getString("title")); } JSONArray jArray = jObject.getJSONArray("names"); if (jArray.length() == songLinks.size()) { for (int i = 0; i < jArray.length(); i++) { String songLink = songLinks.get(i); // If the show has a "selectedSong" meaning that it was opened by // the user clicking on a song link, do a comparison to see // if the song being added is the selected song. If it is, set // selectedPos to the right index so that the song can be played // once the ListView is filled. if (show[0].hasSelectedSong()) { if (songLink.equals(show[0].getSelectedSong())) { selectedPos = i; } } else { selectedPos = -1; } ArchiveSongObj song = new ArchiveSongObj(jArray.get(i).toString(), songLink, showTitle, showIdent); songs.add(song); } } else { } return null; } catch (JSONException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } } catch (XPatherException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } @Override protected void onPostExecute(Void v) { completed = true; parentScreen.setSongs(songs); notifyActivityTaskCompleted(); VibeVault.db.insertRecentShow(taskShow); } // The parent could be null if you changed orientations // and this method was called before the new ShowDetailsScreen // could set itself as this Thread's parent. private void notifyActivityTaskCompleted() { if (parentScreen != null) { parentScreen.onTaskCompleted(); } } // When a ShowDetailsScreen is reconstructed (like after an orientation change), // we call this method on the retained ShowDetailsScreen (if one exists) to set // its parent Activity as the new ShowDetailsScreen because the old one has been destroyed. // This prevents leaking any of the data associated with the old ShowDetailsScreen. private void setActivity(ShowDetailsScreen activity) { this.parentScreen = activity; if (completed) { notifyActivityTaskCompleted(); } } } private class ColoredAdapter extends ArrayAdapter<ArchiveSongObj> { public ColoredAdapter(Context context, int textViewResourceId, List<ArchiveSongObj> objects) { super(context, textViewResourceId, objects); } @Override public View getView(int position, View convertView, ViewGroup parent) { ArchiveSongObj song = downloadLinks.get(position); if (convertView == null) { LayoutInflater vi = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = vi.inflate(R.layout.show_details_screen_row, null); } TextView text = (TextView) convertView.findViewById(R.id.text); text.setText(song.toString()); text.setSelected(true); ImageView icon = (ImageView) convertView.findViewById(R.id.icon); if (song != null) { if (VibeVault.db.songIsDownloaded(song.getFileName())) { icon.setImageDrawable( getBaseContext().getResources().getDrawable(android.R.drawable.star_big_on)); } else { icon.setImageDrawable(null); } } return convertView; } } private ServiceConnection onDService = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder rawBinder) { dService = ((DownloadService.DServiceBinder) rawBinder).getService(); } public void onServiceDisconnected(ComponentName className) { dService = null; } }; private ServiceConnection conn = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { pService = ((PlaybackService.ListenBinder) service).getService(); } @Override public void onServiceDisconnected(ComponentName name) { Log.w(LOG_TAG, "DISCONNECT"); pService = null; } }; private BroadcastReceiver TitleReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { } }; }