com.zion.htf.ui.fragment.ArtistSoundcloudFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.zion.htf.ui.fragment.ArtistSoundcloudFragment.java

Source

/*
 *
 *     Copyright 2013-2014 Yohann Bianchi
 *
 *     This program 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 2 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, write to the Free Software Foundation, Inc.,
 *     51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *     or see <http://www.gnu.org/licenses/>.
 *
 */

package com.zion.htf.ui.fragment;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.AdapterView;
import android.widget.ImageButton;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.ToggleButton;

import com.jakewharton.disklrucache.DiskLruCache;
import com.soundcloud.api.ApiWrapper;
import com.soundcloud.api.Request;
import com.zion.htf.Application;
import com.zion.htf.BuildConfig;
import com.zion.htf.R;
import com.zion.htf.adapter.SoundcloudTracksAdapter;
import com.zion.music.MediaPlayerService;
import com.zion.htf.background.StreamingMediaPlayerService;
import com.zion.htf.data.Artist;
import com.zion.htf.data.SoundcloudTrack;
import com.zion.htf.exception.MissingArgumentException;
import com.zion.htf.ui.AbstractServiceProxyActivity;
import com.zion.music.ServiceUtils;
import com.zion.util.StringUtils;

import org.apache.http.HttpResponse;
import org.apache.http.util.EntityUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.michenux.android.info.VersionUtils;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ArtistSoundcloudFragment extends Fragment
        implements AbstractServiceProxyActivity.ServiceProxyObserver, MediaPlayerService.Client {
    public static final String ARG_ARTIST_ID = "com.zion.htf.arg.artist_id";
    public static final String ARG_SET_ID = "com.zion.htf.arg.set_id";

    private Artist artist;

    private ListView listView;
    private View view;

    private final ArtistSoundcloudFragment.RetrieveTracksTask retrieveTracksTask = new ArtistSoundcloudFragment.RetrieveTracksTask();

    /* Media player controls */
    private ToggleButton playButton;
    private ToggleButton repeatButton;
    private ToggleButton shuffleButton;
    private MediaPlayerService service = null;
    private SeekBar seekBar;
    private TextView timeElapsedTextView;
    private TextView durationTextView;

    private int currentTrackDuration = 0;

    private boolean fragmentReady = false;
    private boolean serviceBound = false;

    /** Indicates whether the user is currently using the seekbar */
    private boolean userSeeking = false;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Bundle args = this.getArguments();
        try {
            if (!args.containsKey(ArtistSoundcloudFragment.ARG_ARTIST_ID)
                    && !args.containsKey(ArtistSoundcloudFragment.ARG_SET_ID))
                throw new MissingArgumentException(String.format(Locale.ENGLISH,
                        "Either %s or %s is required for this Activity to work properly. Please provide any of them.",
                        ArtistDetailsFragment.ARG_SET_ID, ArtistDetailsFragment.ARG_ARTIST_ID));
            if (args.containsKey(ArtistSoundcloudFragment.ARG_ARTIST_ID)) {
                this.artist = Artist.getById(args.getInt(ArtistSoundcloudFragment.ARG_ARTIST_ID));
            } else {
                this.artist = Artist.getBySetId(args.getInt(ArtistSoundcloudFragment.ARG_SET_ID));
            }
        } catch (Exception e) {
            //Report this through Piwik
            if (BuildConfig.DEBUG)
                e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        this.view = inflater.inflate(R.layout.fragment_artist_soundcloud, container, false);

        this.listView = (ListView) this.view.findViewById(R.id.artist_tracks_listview);
        this.listView.setEmptyView(this.view.findViewById(R.id.empty_view_stub));
        this.listView.setOnItemClickListener(new ListViewClickListener());

        // Start fetching tracks (if soundcloud_url isn't empty)
        String soundcloudUrl = this.artist.getSoundcloud();
        boolean malformedSoundcloudUrl = false;
        if (0 < soundcloudUrl.length()) {
            Pattern scUSerIdPattern = Pattern.compile("https?://(?:www\\.)?soundcloud\\.com/(.{3,25})/?$");
            Matcher scUserIdMatcher;
            scUserIdMatcher = scUSerIdPattern.matcher(soundcloudUrl);
            if (scUserIdMatcher.find()) {
                String scUserId = scUserIdMatcher.group(1);
                this.retrieveTracksTask.execute(scUserId);
            } else {
                // Report this through Piwik
                if (BuildConfig.DEBUG)
                    Log.v(ArtistSoundcloudFragment.RetrieveTracksTask.TAG, String.format(Locale.ENGLISH,
                            "Cannot parse \"%s\" to extract a soundcloud username or user_id", soundcloudUrl));
                malformedSoundcloudUrl = true;
            }
        }

        if (0 == soundcloudUrl.length() || malformedSoundcloudUrl) {
            this.setErrorMessage(malformedSoundcloudUrl ? R.string.error_no_soundcloud
                    : R.string.error_finding_soundcloud_profile);
        }

        return this.view;
    }

    @Override
    public void onDestroy() {
        ((AbstractServiceProxyActivity) this.getActivity()).unregisterObserver(this);
        this.retrieveTracksTask.cancel(true);
        super.onDestroy();
    }

    @Override
    public void onResume() {
        super.onResume();
        Log.v("ArtistSoundcloudFragment", "fragment resumed");
        ServiceUtils.notifyForegroundStateChanged(this.getActivity(), true);
        if (this.fragmentReady && !this.serviceBound) {
            this.requestServiceBond(this.getActivity());
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        Log.v("ArtistSoundcloudFragment", "fragment paused");
        ServiceUtils.notifyForegroundStateChanged(this.getActivity(), false);
        if (this.serviceBound) {
            this.service.unregisterClient(this);
            this.requestServiceUnbind(this.getActivity());
        }
    }

    private void setErrorMessage(String message) {
        ProgressBar progressBar = (ProgressBar) this.view.findViewById(R.id.progressBar);
        progressBar.setVisibility(0 == message.length() ? View.VISIBLE : View.GONE);

        TextView messageTextView = (TextView) this.view.findViewById(R.id.message);
        if (null == messageTextView) {
            ViewStub stub = (ViewStub) this.view.findViewById(R.id.empty_view_stub);
            if (null != stub)
                stub.inflate();
            messageTextView = (TextView) this.view.findViewById(R.id.message);
        }
        messageTextView.setVisibility(View.VISIBLE);
        messageTextView.setText(message);
    }

    private void setErrorMessage(int stringResId) {
        this.setErrorMessage(this.getString(stringResId));
    }

    ////////////////////////////////////
    // BEGIN MediaPlayerService Proxy //
    ////////////////////////////////////
    public void start() {
        this.service.startPlayback();
    }

    public void start(int position) {
        this.service.startPlayback(position);
    }

    public void pause() {
        this.service.pausePlayback();
    }

    public void next() {
        this.service.playNext();
    }

    public void prev() {
        this.service.playPrev();
    }

    public void seekTo(int position) {
        this.service.seekTo(position);
    }

    public void shuffle(boolean activate) {
        this.service.setShuffle(activate);
    }

    public void repeat(boolean activate) {
        this.service.setRepeat(activate);
    }
    //////////////////////////////////
    // END MediaPlayerService Proxy //
    //////////////////////////////////

    ///////////////////////////////////////////////////////////////////////
    // BEGIN AbstractServiceProxyActivity.ServiceProxyObserver callbacks //
    ///////////////////////////////////////////////////////////////////////
    @Override
    public void onServiceConnectedToProxy(MediaPlayerService service) {
        this.serviceBound = true;
        Log.v("ArtistSoundcloudFragment", "Got a reference to the service!");
        this.service = service;
        this.service.registerClient(this);

        // Synchronise controls' state with the service
        this.playButton.setChecked(this.service.isPlaying());
        this.shuffleButton.setChecked(this.service.isShuffleActivated());
        this.repeatButton.setChecked(this.service.isRepeatActivated());

        if (this.service.isPlaying() || this.service.isPaused()) {
            int duration = this.service.getDuration();
            this.seekBar.setMax(duration);
            this.seekBar.setProgress(this.service.getCurrentPosition());
            this.onBufferProgressChanged(this.service.getBufferedPercentage());
            this.durationTextView.setText(StringUtils.formatDuration(duration / 1000));
        } else if (this.service.isPreparing()) {
            this.setSeekBarIndeterminate(true);
        }
    }

    @Override
    public void onServiceConnectionLost(ComponentName name) {
        Log.v("ArtistSoundcloudFragment", "Lost the reference to the service :-(");
        this.serviceBound = false;
        this.service = null;
    }
    /////////////////////////////////////////////////////////////////////
    // END AbstractServiceProxyActivity.ServiceProxyObserver callbacks //
    /////////////////////////////////////////////////////////////////////

    ///////////////////////////////////////////////
    // BEGIN MusicPlayerService.Client callbacks //
    ///////////////////////////////////////////////
    @Override
    public void onBufferProgressChanged(int bufferredPercentage) {
        this.seekBar.setSecondaryProgress(bufferredPercentage * this.currentTrackDuration / 100);
    }

    @Override
    public void onPlayerStateChanged(MediaPlayerService.State state) {
        switch (state) {
        case Playing:
            this.setSeekBarIndeterminate(false);
            this.playButton.setChecked(true);
            break;
        case Stopped:
            this.playButton.setChecked(false);
            this.currentTrackDuration = 0;
            this.timeElapsedTextView.setText("");
            this.durationTextView.setText("");
            break;
        case Paused:
            this.playButton.setChecked(false);
            break;
        case Preparing:
            Log.v("ArtistSoundcloudFragment", "Service is now Preparing");
            this.setSeekBarIndeterminate(true);
            this.seekBar.setSecondaryProgress(0);
            this.currentTrackDuration = 0;
            this.timeElapsedTextView.setText("");
            this.durationTextView.setText("");
            break;
        }
    }

    @Override
    public void onPlayerProgressChanged(int progress, int duration) {
        if (0 == this.currentTrackDuration) {
            this.currentTrackDuration = duration;
            this.seekBar.setMax(duration);
            this.durationTextView.setText(StringUtils.formatDuration(duration / 1000));
        }
        if (!this.userSeeking) {
            this.seekBar.setProgress(progress);
        }
    }
    /////////////////////////////////////////////
    // END MusicPlayerService.Client callbacks //
    /////////////////////////////////////////////

    class RetrieveTracksTask extends AsyncTask<String, Void, ArrayList<SoundcloudTrack>> {
        private static final String TAG = "RetrieveTracksTasks";
        private Exception exception;
        private final Context applicationContext = Application.getContext();

        protected ArrayList<SoundcloudTrack> doInBackground(String... args) {
            if (1 != args.length)
                throw new IllegalArgumentException(
                        "The args parameter must contain only one element: the soundcloud username or user_id");
            String scUserId = args[0];

            DiskLruCache lruCache = null;
            try {
                lruCache = DiskLruCache.open(this.applicationContext.getFilesDir(),
                        VersionUtils.getVersionCode(this.applicationContext), 1, 5 * 1024 * 1024);
            } catch (IOException e) {
                Log.e(ArtistSoundcloudFragment.RetrieveTracksTask.TAG, "Error opening the DiskLruCache.");
                if (BuildConfig.DEBUG)
                    e.printStackTrace();
            }

            ArrayList<SoundcloudTrack> tracks = new ArrayList<SoundcloudTrack>();

            ApiWrapper wrapper = new ApiWrapper(
                    ArtistSoundcloudFragment.this.getString(R.string.soundcloud_client_id),
                    ArtistSoundcloudFragment.this.getString(R.string.soundcloud_client_secret), null, null);
            try {
                String url = String.format(Locale.ENGLISH, "/users/%s/tracks.json", scUserId);
                String cacheKey = StringUtils.hashMD5(url);
                String jsonResponse = null;
                if (null != lruCache) {
                    try {
                        DiskLruCache.Snapshot snapshot = lruCache.get(cacheKey);
                        if (null != snapshot)
                            jsonResponse = snapshot.getString(0);
                    } catch (IOException e) {
                        if (BuildConfig.DEBUG)
                            e.printStackTrace();
                        Log.e(ArtistSoundcloudFragment.RetrieveTracksTask.TAG, "Cache hit but unreadable.");
                    }
                }

                if (null == jsonResponse) {
                    HttpResponse trackListResponse = wrapper.get(Request.to(url));
                    jsonResponse = EntityUtils.toString(trackListResponse.getEntity());
                    if (null != lruCache) {
                        try {
                            DiskLruCache.Editor editor = lruCache.edit(cacheKey);
                            editor.set(0, jsonResponse);
                            editor.commit();
                            Log.w(ArtistSoundcloudFragment.RetrieveTracksTask.TAG,
                                    "Cache not hit, but response cached successfully.");
                        } catch (IOException e) {
                            if (BuildConfig.DEBUG)
                                e.printStackTrace();
                            Log.e(ArtistSoundcloudFragment.RetrieveTracksTask.TAG,
                                    "Cache not hit and unable to cache response.");
                        }
                    } else {
                        Log.e(ArtistSoundcloudFragment.RetrieveTracksTask.TAG, "Cache unavailable.");
                    }
                }

                JSONArray trackListJSONArray = new JSONArray(jsonResponse);

                JSONObject trackJSON;
                SoundcloudTrack track;
                int length = trackListJSONArray.length();
                for (int i = 0; i < length; i++) {
                    trackJSON = trackListJSONArray.getJSONObject(i);
                    if ("track".equals(trackJSON.get("kind"))) {
                        track = new SoundcloudTrack(trackJSON.getInt("id"), trackJSON.getString("title"),
                                trackJSON.getString("stream_url"));
                        track.setArtist(ArtistSoundcloudFragment.this.artist.getName());
                        track.setDuration(trackJSON.getInt("duration"));
                        tracks.add(track);
                    } else {
                        if (BuildConfig.DEBUG)
                            Log.v(ArtistSoundcloudFragment.RetrieveTracksTask.TAG, String.format(Locale.ENGLISH,
                                    "Unexpected object kind: %s", trackJSON.get("kind")));
                    }
                }
            } catch (JSONException e) {
                if (BuildConfig.DEBUG)
                    e.printStackTrace();
                this.exception = e;
            } catch (IOException e) {
                if (BuildConfig.DEBUG)
                    e.printStackTrace();
                this.exception = e;
            }

            return tracks;
        }

        protected void onPostExecute(ArrayList<SoundcloudTrack> tracks) {
            if (null == this.exception) {
                Intent serviceIntent = new Intent(ArtistSoundcloudFragment.this.getActivity(),
                        StreamingMediaPlayerService.class);
                ArtistSoundcloudFragment.this.getActivity().startService(serviceIntent);

                ArtistSoundcloudFragment.this.listView
                        .setAdapter(new SoundcloudTracksAdapter(Application.getContext(), tracks));

                ArtistSoundcloudFragment.this.fragmentReady = true;

                Activity activity = ArtistSoundcloudFragment.this.getActivity();
                ArtistSoundcloudFragment.this.startService(activity, tracks);
                ArtistSoundcloudFragment.this.requestServiceBond(activity);

                // Initialize the media player's controls
                ArtistSoundcloudFragment.this.initMediaController();
            } else {
                ArtistSoundcloudFragment.this.setErrorMessage(R.string.error_finding_soundcloud_profile);
                // Report through Piwik
            }
        }
    }

    /**
     * Starts the service and request the activity to bind to it
     * @param activity the fragment's activity
     * @param tracks the list of {@link com.zion.htf.data.SoundcloudTrack}s to pass to the service
     */
    private void startService(Activity activity, ArrayList<SoundcloudTrack> tracks) {
        // Start the service, passing it the playlist
        Intent intent = new Intent(activity, MediaPlayerService.class);
        intent.putExtra(MediaPlayerService.EXTRA_TRACKS, tracks);
        intent.setAction(MediaPlayerService.ACTION_QUEUE_TRACKS);
        intent.putExtra(MediaPlayerService.EXTRA_ARTIST_ID, this.artist.getId());
        intent.putExtra(MediaPlayerService.EXTRA_ARTIST_PHOTO, this.artist.getPictureResourceId());
        activity.startService(intent);
    }

    /**
     * Requests the activity to bind to the service, to allow easier communication with the service.
     * @param activity the current fragment's activity, acting as a proxy to the service
     */
    private void requestServiceBond(Activity activity) {
        ArtistSoundcloudFragment.MediaPlayerServiceProxy serviceProxy = (ArtistSoundcloudFragment.MediaPlayerServiceProxy) activity;
        ((AbstractServiceProxyActivity) activity).registerObserver(this);
        activity.bindService(new Intent(activity, MediaPlayerService.class), serviceProxy.getServiceConnection(),
                Context.BIND_AUTO_CREATE);
    }

    /**
     * Requests the activity to unbind from the service.
     * @param activity the current fragment's activity, acting as a proxy to the service
     */
    private void requestServiceUnbind(Activity activity) {
        ArtistSoundcloudFragment.MediaPlayerServiceProxy serviceProxy = (ArtistSoundcloudFragment.MediaPlayerServiceProxy) activity;
        AbstractServiceProxyActivity serviceProxyActivity = (AbstractServiceProxyActivity) activity;
        if (serviceProxyActivity.isServiceBound()) {
            activity.unbindService(serviceProxy.getServiceConnection());
        } else {
            Log.w("ArtistSoundcloudFragment", "Useless call");
        }
    }

    @SuppressLint("NewApi")
    private void setSeekBarIndeterminate(boolean indeterminate) {
        this.seekBar.setIndeterminate(indeterminate);
        if (16 <= Build.VERSION.SDK_INT)
            this.seekBar.getThumb().mutate().setAlpha(indeterminate ? 0x00 : 0xFF);
    }

    private void initMediaController() {
        ViewStub stub = (ViewStub) this.view.findViewById(R.id.mediaplayer_controller_view_stub);
        if (null != stub) {
            ArtistSoundcloudFragment.Listeners listeners = new ArtistSoundcloudFragment.Listeners();

            stub.inflate();

            ToggleButton playButton = (ToggleButton) this.view.findViewById(R.id.button_play);
            ToggleButton shuffleButton = (ToggleButton) this.view.findViewById(R.id.button_shuffle);
            ToggleButton repeatButton = (ToggleButton) this.view.findViewById(R.id.button_repeat);
            ImageButton prevButton = (ImageButton) this.view.findViewById(R.id.button_prev);
            ImageButton nextButton = (ImageButton) this.view.findViewById(R.id.button_next);

            // Set button click listeners
            playButton.setOnClickListener(listeners);
            shuffleButton.setOnClickListener(listeners);
            repeatButton.setOnClickListener(listeners);
            prevButton.setOnClickListener(listeners);
            nextButton.setOnClickListener(listeners);

            // Get a reference to the mediaplayer controls
            this.seekBar = (SeekBar) this.view.findViewById(R.id.seekbar);
            this.timeElapsedTextView = (TextView) this.view.findViewById(R.id.elapsed_time);
            this.durationTextView = (TextView) this.view.findViewById(R.id.duration);
            this.playButton = playButton;
            this.shuffleButton = shuffleButton;
            this.repeatButton = repeatButton;

            this.seekBar.setOnSeekBarChangeListener(listeners);

        }
    }

    /**
     * Interface used to get a reference to the {@link com.zion.htf.background.StreamingMediaPlayerService} in order to interact with it.
     * Classes implementing this interface should keep a static reference to the {@link com.zion.htf.background.StreamingMediaPlayerService}
     */
    public interface MediaPlayerServiceProxy {
        /**
         * Return a reference to the {@link com.zion.htf.background.StreamingMediaPlayerService}
         * @return A {@link java.lang.ref.WeakReference} to the {@link com.zion.htf.background.StreamingMediaPlayerService}
         * @throws java.lang.IllegalStateException When trying to get a reference to a {@link com.zion.htf.background.StreamingMediaPlayerService} before it is bound.
         */
        ServiceConnection getServiceConnection();
    }

    private class ListViewClickListener implements ListView.OnItemClickListener {
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            ArtistSoundcloudFragment.this.start(position);
        }
    }

    private class Listeners implements View.OnClickListener, SeekBar.OnSeekBarChangeListener {
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
            case R.id.button_prev:
                ArtistSoundcloudFragment.this.prev();
                break;
            case R.id.button_next:
                ArtistSoundcloudFragment.this.next();
                break;
            case R.id.button_shuffle:
                ArtistSoundcloudFragment.this.shuffle(ArtistSoundcloudFragment.this.shuffleButton.isChecked());
                break;
            case R.id.button_repeat:
                ArtistSoundcloudFragment.this.repeat(ArtistSoundcloudFragment.this.repeatButton.isChecked());
                break;
            case R.id.button_play:
                if (ArtistSoundcloudFragment.this.playButton.isChecked())
                    ArtistSoundcloudFragment.this.start();
                else
                    ArtistSoundcloudFragment.this.pause();
                break;
            }
        }

        @Override
        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            ArtistSoundcloudFragment.this.timeElapsedTextView.setText(StringUtils.formatDuration(progress / 1000));
        }

        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
            ArtistSoundcloudFragment.this.userSeeking = true;
        }

        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
            ArtistSoundcloudFragment.this.userSeeking = false;
            ArtistSoundcloudFragment.this.seekTo(seekBar.getProgress());
        }
    }
}