com.cyanogenmod.effem.FmRadio.java Source code

Java tutorial

Introduction

Here is the source code for com.cyanogenmod.effem.FmRadio.java

Source

/*
 * Copyright (C) 2011 The Android Open Source Project
 *               2013 Grigori Goronzy <greg@chown.ath.cx>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.cyanogenmod.effem;

import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.content.Context;
import android.content.SharedPreferences;
import android.media.MediaPlayer;
import android.os.*;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.widget.ArrayAdapter;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import com.stericsson.hardware.fm.FmBand;
import android.media.AudioManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.ComponentName;
import android.graphics.Typeface;

import java.io.IOException;
import java.util.Comparator;
import org.json.*;

public class FmRadio extends Activity implements FmRadioService.Callbacks, ServiceConnection {

    private static final String LOG_TAG = "Effem";
    public static final String PREFS_NAME = "FMRadioPrefsFile";

    // Menu identifiers
    private static final int BASE_OPTION_MENU = 0;
    private static final int BAND_SELECTION_MENU = 1;
    private static final int LOUDSPEAKER_SELECTION_MENU = 2;
    private static final int STATION_SELECTION_MENU = 3;

    public static final int FM_BAND = Menu.FIRST;
    public static final int BAND_US = Menu.FIRST + 1;
    public static final int BAND_EU = Menu.FIRST + 2;
    public static final int BAND_JAPAN = Menu.FIRST + 3;
    public static final int BAND_CHINA = Menu.FIRST + 4;
    public static final int OUTPUT_SOUND = Menu.FIRST + 5;
    public static final int OUTPUT_HEADSET = Menu.FIRST + 6;
    public static final int OUTPUT_SPEAKER = Menu.FIRST + 7;
    public static final int STATION_SELECT = Menu.FIRST + 8;
    public static final int STATION_SELECT_MENU_ITEMS = STATION_SELECT + 1;

    // Application context
    private Context context;

    // Views for frequency, station name, program type and radio text
    private TextView mFrequencyTextView;
    private TextView mStationNameTextView;
    private TextView mProgramTypeTextView;
    private TextView mStationInfoTextView;

    // FM state
    private HandlerThread mWorker;
    private Handler mWorkerHandler;
    private FmRadioService mService;
    private int mCurrentFrequency;
    private boolean mFirstStart = true;
    private int mSelectedBand;
    private int mSelectedOutput;

    // Array of the available stations in MHz
    private ArrayAdapter<MenuTuple> mMenuAdapter;

    /**
     * Required method from parent class
     *
     * @param icicle - The previous instance of this app
     */
    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        context = getApplicationContext();
        setContentView(R.layout.main);

        // restore preferences
        SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
        mSelectedBand = settings.getInt("selectedBand", 1);
        mCurrentFrequency = settings.getInt("currentFrequency", 0);
        if (context.getResources().getBoolean(R.bool.speaker_supported)) {
            mSelectedOutput = settings.getInt("selectedOutput", 0) > 0 ? 1 : 0;
        }

        // misc setup
        setVolumeControlStream(AudioManager.STREAM_MUSIC);
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

        // worker thread for async execution of FM stuff
        mWorker = new HandlerThread("EffemWorker");
        mWorker.start();
        mWorkerHandler = new Handler(mWorker.getLooper());

        // ui preparations
        setupButtons();
    }

    /**
     * Starts up the listeners and the FM radio if it isn't already active
     */
    @Override
    protected void onResume() {
        super.onResume();
        Log.i(LOG_TAG, "onResume");

        // start and bind to service
        startService(new Intent(this, FmRadioService.class));
        bindService(new Intent(this, FmRadioService.class), this, Context.BIND_AUTO_CREATE);
    }

    @Override
    public void onServiceConnected(ComponentName component, IBinder binder) {
        mService = ((FmRadioService.LocalBinder) binder).getService();
        // start radio on initial start
        mWorkerHandler.post(new Runnable() {
            public void run() {
                if (mFirstStart)
                    mService.startRadio(mSelectedBand, mCurrentFrequency, mSelectedOutput);
                mService.resumeCallbacks();
                mService.setCallbacks(FmRadio.this);
            }
        });
        mFirstStart = false;
    }

    @Override
    public void onServiceDisconnected(ComponentName component) {
        mService = null;
    }

    /**
     * Stops the FM Radio listeners and unbinds/stops service
     */
    @Override
    protected void onPause() {
        super.onPause();
        Log.i(LOG_TAG, "onPause");

        // suspend callbacks to save power
        // especially, this will disable RDS
        mService.suspendCallbacks();

        // if no playback is going on, the service can exit
        if (mService.isStarted() == false)
            stopService(new Intent(this, FmRadioService.class));

        // unbind from service
        unbindService(this);
    }

    /**
     * Saves the FmBand for next time the program is used and closes the radio
     * and media player.
     */
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mWorker.quit();

        // save preferences
        SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
        SharedPreferences.Editor editor = settings.edit();
        editor.putInt("selectedBand", mSelectedBand);
        editor.putInt("currentFrequency", mCurrentFrequency);
        if (context.getResources().getBoolean(R.bool.speaker_supported)) {
            editor.putInt("selectedOutput", mSelectedOutput);
        }
        try {
            JSONObject conf = new JSONObject();
            JSONArray stations = new JSONArray();
            conf.put("stations", stations);
            for (int i = 0; i < mMenuAdapter.getCount(); i++)
                stations.put(mMenuAdapter.getItem(i).toJSON());
            editor.putString("stations", conf.toString());
        } catch (JSONException e) {
            Log.e(LOG_TAG, "Failed to save station list");
        }
        editor.commit();
    }

    @Override
    public void onReceiverStateChanged(boolean state) {
        Log.i(LOG_TAG, "onReceiverStateChanged " + state);

        // switch button image for play/pause
        ImageButton favorite = (ImageButton) findViewById(R.id.Favorite);
        ImageButton pause = (ImageButton) findViewById(R.id.Pause);
        if (state == true) {
            pause.setImageResource(R.drawable.pausebutton);
            favorite.setEnabled(true);
        } else {
            pause.setImageResource(R.drawable.playbutton);
            favorite.setEnabled(false);
        }
    }

    @Override
    public void onFrequencyChanged(int frequency, int offset) {
        mCurrentFrequency = frequency;
        String freqFormatted = FmUtils.formatFrequency(offset, mCurrentFrequency);

        ((ImageButton) findViewById(R.id.ScanUp)).setEnabled(true);
        ((ImageButton) findViewById(R.id.ScanDown)).setEnabled(true);

        mFrequencyTextView.setText(freqFormatted);
        mStationInfoTextView.setText("");
        mStationNameTextView.setText(R.string.no_rds);
        mProgramTypeTextView.setText("");

        final ImageButton favorite = (ImageButton) findViewById(R.id.Favorite);
        if (getFavorite(frequency))
            favorite.setImageResource(R.drawable.favoritebuttonpress);
        else
            favorite.setImageResource(R.drawable.favoritebutton);
    }

    @Override
    public void onRdsDataAvailable(Bundle rdsData) {
        if (rdsData.containsKey("PSN")) {
            mStationNameTextView.setText(rdsData.getString("PSN").trim());
        }

        if (rdsData.containsKey("RT")) {
            String text = rdsData.getString("RT").trim();

            // only update if the text differs, otherwise this messes
            // up the marquee
            if (!mStationInfoTextView.getText().equals(text))
                mStationInfoTextView.setText(text);
        }

        if (rdsData.containsKey("PTY")) {
            int pty = rdsData.getShort("PTY");
            if (pty > 0)
                mProgramTypeTextView.setText(FmUtils.getPTYName(this, pty));
            else
                mProgramTypeTextView.setText("");
        }
    }

    /**
     * Sets up the buttons and their listeners
     */
    private void setupButtons() {
        // populate favorites menu
        mMenuAdapter = new ArrayAdapter<MenuTuple>(this, android.R.layout.simple_spinner_item);

        try {
            SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
            JSONObject conf = new JSONObject(settings.getString("stations", ""));
            JSONArray stations = conf.getJSONArray("stations");
            for (int i = 0; i < stations.length(); i++) {
                MenuTuple mt = MenuTuple.fromJSON(stations.getJSONObject(i));
                mMenuAdapter.add(mt);
            }
        } catch (JSONException e) {
            Log.e(LOG_TAG, "Failed to load station list");
        }

        // get references to buttons
        mFrequencyTextView = (TextView) findViewById(R.id.FrequencyTextView);
        mStationNameTextView = (TextView) findViewById(R.id.PSNTextView);
        mStationInfoTextView = (TextView) findViewById(R.id.RTTextView);
        mProgramTypeTextView = (TextView) findViewById(R.id.PTYTextView);
        final ImageButton scanUp = (ImageButton) findViewById(R.id.ScanUp);
        final ImageButton scanDown = (ImageButton) findViewById(R.id.ScanDown);
        final ImageButton pause = (ImageButton) findViewById(R.id.Pause);
        final ImageButton favorite = (ImageButton) findViewById(R.id.Favorite);
        mStationInfoTextView.setSelected(true);

        // set typeface for station frequency widget
        // this is done here instead of layout xml to keep ICS compatibility
        mFrequencyTextView.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));

        scanUp.setOnLongClickListener(new OnLongClickListener() {
            public boolean onLongClick(View v) {
                scanUp.setEnabled(false);
                mWorkerHandler.post(new Runnable() {
                    public void run() {
                        mService.changeFrequency(FmRadioService.SEEK_STEPUP, 0);
                    }
                });
                return true;
            }
        });

        scanDown.setOnLongClickListener(new OnLongClickListener() {
            public boolean onLongClick(View v) {
                scanDown.setEnabled(false);
                mWorkerHandler.post(new Runnable() {
                    public void run() {
                        mService.changeFrequency(FmRadioService.SEEK_STEPDOWN, 0);
                    }
                });
                return true;
            }
        });

        scanUp.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                mWorkerHandler.post(new Runnable() {
                    public void run() {
                        mService.startRadio(mSelectedBand, mCurrentFrequency, mSelectedOutput);
                        if (mService.isStarted())
                            mService.changeFrequency(FmRadioService.SEEK_SCANUP, 0);
                    }
                });
                scanUp.setEnabled(false);
            }
        });

        scanDown.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                mWorkerHandler.post(new Runnable() {
                    public void run() {
                        mService.startRadio(mSelectedBand, mCurrentFrequency, mSelectedOutput);
                        if (mService.isStarted())
                            mService.changeFrequency(FmRadioService.SEEK_SCANDOWN, 0);
                    }
                });
                scanDown.setEnabled(false);
            }
        });

        pause.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                if (mService.isStarted())
                    mService.stopRadio();
                else {
                    mWorkerHandler.post(new Runnable() {
                        public void run() {
                            mService.startRadio(mSelectedBand, mCurrentFrequency, mSelectedOutput);
                        }
                    });
                }
            }
        });

        favorite.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                toggleFavorite(v, mCurrentFrequency);
            }
        });
    }

    private boolean getFavorite(int frequency) {
        for (int i = 0; i < mMenuAdapter.getCount(); i++) {
            if (mMenuAdapter.getItem(i).frequency == frequency)
                return true;
        }
        return false;
    }

    private void toggleFavorite(View v, int frequency) {
        final ImageButton favorite = (ImageButton) findViewById(R.id.Favorite);

        // check if it already exists
        if (getFavorite(frequency)) {
            // favorite should be removed
            for (int i = 0; i < mMenuAdapter.getCount(); i++) {
                if (mMenuAdapter.getItem(i).frequency == frequency)
                    mMenuAdapter.remove(mMenuAdapter.getItem(i));
            }
            invalidateOptionsMenu();
            favorite.setImageResource(R.drawable.favoritebutton);
        } else {
            // insert favorite
            int offset = mService.getChannelOffset();
            String freqFormatted = FmUtils.formatFrequency(offset, frequency);
            String freqString = "";
            if (mStationNameTextView.getText() != getText(R.string.no_rds)) {
                freqString = mStationNameTextView.getText() + " (" + freqFormatted + ")";
            } else
                freqString = freqFormatted;
            mMenuAdapter.add(new MenuTuple(frequency, freqString));
            // sort (ascending)
            mMenuAdapter.sort(new Comparator<MenuTuple>() {
                public int compare(MenuTuple l, MenuTuple r) {
                    return l.frequency - r.frequency;
                }

                public boolean equals(Object obj) {
                    return this == obj;
                }
            });
            invalidateOptionsMenu();
            favorite.setImageResource(R.drawable.favoritebuttonpress);
        }
    }

    /**
     * Sets up the options menu when the menu button is pushed, dynamic
     * population of the station select menu
     */
    public boolean onPrepareOptionsMenu(Menu menu) {
        menu.clear();
        boolean result = super.onCreateOptionsMenu(menu);

        // Create and populate the band selection menu
        SubMenu subMenu = menu.addSubMenu(BASE_OPTION_MENU, FM_BAND, Menu.NONE, R.string.band_select);
        subMenu.setIcon(android.R.drawable.ic_menu_mapmode);
        subMenu.add(BAND_SELECTION_MENU, BAND_US, Menu.NONE, R.string.band_us);
        subMenu.add(BAND_SELECTION_MENU, BAND_EU, Menu.NONE, R.string.band_eu);
        subMenu.add(BAND_SELECTION_MENU, BAND_JAPAN, Menu.NONE, R.string.band_ja);
        subMenu.add(BAND_SELECTION_MENU, BAND_CHINA, Menu.NONE, R.string.band_ch);
        subMenu.setGroupCheckable(BAND_SELECTION_MENU, true, true);
        subMenu.getItem(mSelectedBand).setChecked(true);

        // Create and populate the speaker/headset selection menu if speaker supported
        if (context.getResources().getBoolean(R.bool.speaker_supported)) {
            subMenu = menu.addSubMenu(BASE_OPTION_MENU, OUTPUT_SOUND, Menu.NONE, R.string.output_select);
            subMenu.setIcon(android.R.drawable.ic_menu_mapmode);
            subMenu.add(LOUDSPEAKER_SELECTION_MENU, OUTPUT_HEADSET, Menu.NONE, R.string.output_select_default);
            subMenu.add(LOUDSPEAKER_SELECTION_MENU, OUTPUT_SPEAKER, Menu.NONE, R.string.output_select_loudspeaker);
            subMenu.setGroupCheckable(LOUDSPEAKER_SELECTION_MENU, true, true);
            subMenu.getItem(mSelectedOutput).setChecked(true);
        }

        // Create the station select menu
        subMenu = menu.addSubMenu(BASE_OPTION_MENU, STATION_SELECT, Menu.NONE, R.string.station_select);
        subMenu.getItem().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);

        // Dynamically populate the station select menu
        if (mMenuAdapter.isEmpty()) {
            subMenu.setGroupEnabled(STATION_SELECTION_MENU, false);
        } else {
            subMenu.setGroupEnabled(STATION_SELECTION_MENU, true);
            for (int i = 0; i < mMenuAdapter.getCount(); i++) {
                subMenu.add(STATION_SELECTION_MENU, STATION_SELECT_MENU_ITEMS + i, Menu.NONE,
                        mMenuAdapter.getItem(i).toString());
            }
        }
        return result;
    }

    public int getSelectStationMenuItem(MenuItem item) {
        return item.getItemId() - STATION_SELECT_MENU_ITEMS;
    }

    /**
     * React to a selection in the option menu
     */
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {

        switch (item.getGroupId()) {
        case BAND_SELECTION_MENU:
            switch (item.getItemId()) {
            case BAND_US:
                mSelectedBand = FmBand.BAND_US;
                item.setChecked(true);
                break;
            case BAND_EU:
                mSelectedBand = FmBand.BAND_EU;
                item.setChecked(true);
                break;
            case BAND_JAPAN:
                mSelectedBand = FmBand.BAND_JAPAN;
                item.setChecked(true);
                break;
            case BAND_CHINA:
                mSelectedBand = FmBand.BAND_CHINA;
                item.setChecked(true);
                break;
            default:
                break;
            }
            mWorkerHandler.post(new Runnable() {
                public void run() {
                    mService.stopRadio();
                    mService.startRadio(mSelectedBand, 0, mSelectedOutput);
                }
            });
            break;

        case LOUDSPEAKER_SELECTION_MENU:
            mSelectedOutput = (item.getItemId() == OUTPUT_HEADSET) ? 0 : 1;
            mWorkerHandler.post(new Runnable() {
                public void run() {
                    mService.stopRadio();
                    mService.startRadio(mSelectedBand, mCurrentFrequency, mSelectedOutput);
                }
            });
            break;

        case STATION_SELECTION_MENU:
            final int freq = mMenuAdapter.getItem(getSelectStationMenuItem(item)).frequency;
            mWorkerHandler.post(new Runnable() {
                public void run() {
                    if (!mService.isStarted())
                        mService.startRadio(mSelectedBand, freq, mSelectedOutput);
                    else
                        mService.changeFrequency(FmRadioService.SEEK_ABSOLUTE, freq);
                }
            });
            break;
        default:
            break;
        }
        return super.onOptionsItemSelected(item);
    }
}

class MenuTuple {
    public int frequency;
    public String name;

    public MenuTuple(int frequency, String name) {
        this.frequency = frequency;
        this.name = name;
    }

    public String toString() {
        return name;
    }

    // JSON

    public JSONObject toJSON() throws JSONException {
        JSONObject json = new JSONObject();
        json.put("frequency", frequency);
        json.put("name", name);
        return json;
    }

    public static MenuTuple fromJSON(JSONObject json) throws JSONException {
        MenuTuple mt = new MenuTuple(json.getInt("frequency"), json.getString("name"));
        return mt;
    }
}