/* * Copyright (c) 2013, Sorokin Alexander (uas.sorokin@gmail.com) * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * 3. The names of the authors may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.uas.media.aimp.mobile; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.ProgressDialog; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.FragmentTransaction; import android.util.TypedValue; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.ImageView; import android.widget.TextView; import com.actionbarsherlock.app.ActionBar; import com.actionbarsherlock.app.SherlockFragmentActivity; import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuItem; import com.bugsense.trace.BugSenseHandler; import com.uas.media.aimp.AimpPlayerInstance; import com.uas.media.aimp.R; import com.uas.media.aimp.api.IPlugin; import com.uas.media.aimp.api.impl.WebCtlPlugin; import com.uas.media.aimp.api.models.Playlist; import com.uas.media.aimp.api.models.Song; import com.uas.media.aimp.player.*; import com.uas.media.aimp.utils.*; import net.robotmedia.billing.BillingController; import net.robotmedia.billing.BillingRequest; import net.robotmedia.billing.helper.AbstractBillingObserver; import net.robotmedia.billing.model.Transaction; /** * User: uas.sorokin@gmail.com */ public class MainActivity extends SherlockFragmentActivity { private static final long DOUBLE_CLICK_DELAY = 350; private static final int MIN_KEYBOARD_HEIGHT_IN_DP = 120; static class ViewHolder { public View activityRootView; public ViewGroup connectionWrapper; public ViewGroup plsWrapper; public ViewGroup controllerWrapper; public ViewGroup panel; public ViewGroup initPls; public TextView timer; public ImageView playControllerIcon; public ImageView commonControllerIcon; public View playControllerSwitcher; public View commonControllerSwitcher; public ActionBar actionBar; public ImageView connectionIcon; } private ViewHolder mViewHolder; private PlayControllerFragment fPlayControllerFragment; private CommonControllerFragment fCommonControllerFragment; protected PlaylistsListFragment fPlaylistsList; private ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener; private AbstractBillingObserver mBillingObserver; private PlayerMenu.OnChooseListener mMenuListener; private AimpPlayer mAimpPlayer; private VolumeUpdater tVolumeUpdater; private ConnectionListener mConnectionListener; private StateObserver mStateObserver; private long mTimerLastClick; private AlertDialog mMenuDialog; private Dialog mConnectionDialog; private boolean mIsBillingSupported; private boolean mIsKeyboardDisplayed; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.mobile_player); mAimpPlayer = AimpPlayerInstance.get(); mOnGlobalLayoutListener = new OnGlobalLayoutListenerImpl(); mBillingObserver = new AbstractBillingObserverImpl(this); mMenuListener = new PlayerMenuChooseImpl(); mTimerLastClick = System.currentTimeMillis(); tVolumeUpdater = new VolumeUpdater(); mIsKeyboardDisplayed = false; mConnectionListener = new ConnectionListenerImpl(); mStateObserver = new StateObserverImpl(); initViewHolder(); initUi(); initUiEvents(); mAimpPlayer.registerConnectionListener(mConnectionListener); mAimpPlayer.registerStateObserver(mStateObserver); } @Override protected void onStart() { super.onStart(); // init billing mIsBillingSupported = false; BillingController.registerObserver(mBillingObserver); BillingController.checkBillingSupported(this); BillingController.checkSubscriptionSupported(this); // init bugsense BugSenseHandler.initAndStartSession(getApplicationContext(), Bugsense.API_KEY); } @Override protected void onResume() { super.onResume(); mViewHolder.activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener); } @Override protected void onPause() { super.onPause(); mViewHolder.activityRootView.getViewTreeObserver().removeGlobalOnLayoutListener(mOnGlobalLayoutListener); } @Override protected void onStop() { super.onStop(); BillingController.unregisterObserver(mBillingObserver); BugSenseHandler.closeSession(getApplicationContext()); } @Override protected void onDestroy() { super.onDestroy(); mAimpPlayer.unregisterConnectionListener(mConnectionListener); mAimpPlayer.unregisterStateObserver(mStateObserver); mAimpPlayer = null; } // ============================================================ // =========== INIT // ============================================================ protected void initViewHolder() { mViewHolder = new ViewHolder(); mViewHolder.activityRootView = findViewById(R.id.player_wrapper); mViewHolder.connectionWrapper = (ViewGroup) findViewById(R.id.connection_wrapper); mViewHolder.plsWrapper = (ViewGroup) findViewById(R.id.pls_wrapper); mViewHolder.controllerWrapper = (ViewGroup) findViewById(R.id.controller_wrapper); mViewHolder.panel = (ViewGroup) findViewById(R.id.panel); mViewHolder.initPls = (ViewGroup) findViewById(R.id.init_pls); mViewHolder.timer = (TextView) findViewById(R.id.timer); mViewHolder.playControllerIcon = (ImageView) findViewById(R.id.play_controller_icon); mViewHolder.commonControllerIcon = (ImageView) findViewById(R.id.common_controller_icon); mViewHolder.playControllerSwitcher = findViewById(R.id.play_controller_switcher); mViewHolder.commonControllerSwitcher = findViewById(R.id.common_controller_switcher); mViewHolder.actionBar = getSupportActionBar(); mViewHolder.connectionIcon = (ImageView) findViewById(R.id.connection); } protected void initUiEvents() { mViewHolder.connectionIcon.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onConnectionClicked(); } }); mViewHolder.playControllerSwitcher.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onPlayControllerSwitcherClicked(); } }); mViewHolder.commonControllerSwitcher.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onCommonControllerSwitcherClicked(); } }); mViewHolder.timer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onTimerClicked(); } }); } // ============================================================ // =========== UI EVENTS // ============================================================ protected void onConnectionClicked() { doConnect(); } protected void onPlayControllerSwitcherClicked() { if (fPlayControllerFragment == null) { if (fCommonControllerFragment != null) { deactivateCommonController(); } activatePlayController(); } else { deactivatePlayController(); } } protected void onCommonControllerSwitcherClicked() { if (fCommonControllerFragment == null) { if (fPlayControllerFragment != null) { deactivatePlayController(); } activateCommonController(); } else { deactivateCommonController(); } } protected void onTimerClicked() { if (System.currentTimeMillis() - mTimerLastClick < DOUBLE_CLICK_DELAY && mAimpPlayer.isConnected()) { scrollToCurrent(); } mTimerLastClick = System.currentTimeMillis(); } // ============================================================ // =========== INIT UI // ============================================================ protected void initUi() { if (mAimpPlayer.isConnected()) { initUiWhenConnected(); } else { initUiWhenDisconnected(); } } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getSupportMenuInflater(); inflater.inflate(R.menu.action_bar, menu); return true; } @Override public boolean onMenuItemSelected(int featureId, MenuItem item) { switch (item.getItemId()) { case R.id.others: showPopupDialog(); return true; default: return super.onMenuItemSelected(featureId, item); } } protected void initUiWhenConnected() { mViewHolder.panel.setVisibility(View.VISIBLE); mViewHolder.connectionWrapper.setVisibility(View.GONE); mViewHolder.connectionIcon.setVisibility(View.GONE); activatePlaylistsList(); } protected void initUiWhenDisconnected() { mViewHolder.panel.setVisibility(View.GONE); mViewHolder.connectionWrapper.setVisibility(View.VISIBLE); mViewHolder.connectionIcon.setVisibility(View.VISIBLE); if (fPlayControllerFragment != null) { deactivatePlayController(); } if (fCommonControllerFragment != null) { deactivateCommonController(); } if (fPlaylistsList != null) { deactivatePlaylistsList(); } } // ============================================================ // =========== CONTROLLER's SWITCH LOGIC // ============================================================ protected void activatePlayController() { fPlayControllerFragment = new PlayControllerFragment(); FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); ft.replace(mViewHolder.controllerWrapper.getId(), fPlayControllerFragment); ft.commitAllowingStateLoss(); mViewHolder.playControllerIcon.setImageResource(R.drawable.btn_play_controller_active); } protected void deactivatePlayController() { FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); ft.remove(fPlayControllerFragment); ft.commitAllowingStateLoss(); fPlayControllerFragment = null; mViewHolder.playControllerIcon.setImageResource(R.drawable.btn_play_controller_default); } protected void activateCommonController() { fCommonControllerFragment = new CommonControllerFragment(); FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); ft.replace(mViewHolder.controllerWrapper.getId(), fCommonControllerFragment); ft.commitAllowingStateLoss(); mViewHolder.commonControllerIcon.setImageResource(R.drawable.btn_common_controller_active); } protected void deactivateCommonController() { FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); ft.remove(fCommonControllerFragment); ft.commitAllowingStateLoss(); fCommonControllerFragment = null; mViewHolder.commonControllerIcon.setImageResource(R.drawable.btn_common_controller_default); } protected void activatePlaylistsList() { mViewHolder.initPls.setVisibility(View.VISIBLE); fPlaylistsList = new PlaylistsListFragment(); FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); ft.replace(mViewHolder.plsWrapper.getId(), fPlaylistsList); ft.commitAllowingStateLoss(); fPlaylistsList.setOnInitCompleteListener(new PlaylistsListFragment.OnInitCompletedListener() { @Override public void onInitCompleted() { fPlaylistsList.setOnInitCompleteListener(null); mViewHolder.initPls.setVisibility(View.GONE); } }); } protected void deactivatePlaylistsList() { FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); ft.remove(fPlaylistsList); ft.commitAllowingStateLoss(); fPlaylistsList = null; } public boolean isControllerVisible() { return fPlayControllerFragment != null || fCommonControllerFragment != null; } // ============================================================ // =========== CONNECTION PROCESSOR // ============================================================ protected void doConnect() { try { mAimpPlayer.connect( getApplicationContext(), new WebCtlPlugin(Settings.getHost(), Settings.getPort()), Settings.buildSyncParams() ); } catch (AimpException ex) { // already connecting, ignore the exception //throw new RuntimeException(ex); } } protected void doDisconnect() { try { mAimpPlayer.disconnect(); } catch (AimpException ex) { // already connecting, ignore the exception //throw new RuntimeException(ex); } } protected Dialog buildConnectionDialog() { ProgressDialog pd = new ProgressDialog(this); pd.setTitle(R.string.connecting); pd.setMessage(getString(R.string.connecting)); pd.setProgressStyle(ProgressDialog.STYLE_SPINNER); pd.setIndeterminate(true); pd.setCancelable(true); pd.setOnCancelListener(new Dialog.OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { doDisconnect(); } }); return pd; } // ============================================================ // =========== UI UPDATES & COMMANDS // ============================================================ protected void setTimerValue(Song song, int playPosition) { if (song == null) { mViewHolder.timer.setText(""); } else { int songDuration = song.getDuration(); String value = getString( R.string.timer_format, playPosition / 60, playPosition % 60, songDuration / 60, songDuration % 60 ); mViewHolder.timer.setText(value); } } protected void setElementVisibilityForSearch(boolean isKeyboardVisible) { if (isKeyboardVisible) { if (fPlayControllerFragment != null) { deactivatePlayController(); } if (fCommonControllerFragment != null) { deactivateCommonController(); } } mViewHolder.panel.setVisibility(isKeyboardVisible ? View.GONE : View.VISIBLE); } protected void scrollToCurrent() { Playlist currPl = mAimpPlayer.getCurrentPlaylist(); Song currSong = mAimpPlayer.getCurrentSong(); if (currSong != null) { fPlaylistsList.scrollTo(currPl, currSong); } } // ============================================================ // =========== STATE OBSERVER // ============================================================ class StateObserverImpl extends StateObserverViaHandler { @Override public void onSongPlayPositionChanged(Playlist playlist, Song song, int position, double percentage) { setTimerValue(song, position); } @Override public void onSongChanged(Playlist playlist, Song song, int position, double percentage) { if (isControllerVisible()) { scrollToCurrent(); } } } // ============================================================ // =========== CONNECTION LISTENER // ============================================================ class ConnectionListenerImpl extends ConnectionListenerViaHandler { @Override public void onConnectionStatusChanged(IPlugin plugin, AimpPlayer.ConnectionStatus status) { switch (status) { case CONNECTING: mViewHolder.connectionIcon.setVisibility(View.GONE); mConnectionDialog = buildConnectionDialog(); mConnectionDialog.show(); break; case CONNECTED: closeConnectionDialog(); initUiWhenConnected(); break; case DISCONNECTED: initUiWhenDisconnected(); break; default: break; } } @Override public void onAimpNotFound(IPlugin plugin) { closeConnectionDialog(); createConnectionErrorDialog( R.string.error, R.string.error_aimp_not_found ).show(); } @Override public void onHostNotFound(IPlugin plugin) { closeConnectionDialog(); createConnectionErrorDialog( R.string.error, R.string.error_host_not_found ).show(); } @Override public void onUnresolvedError(IPlugin plugin, Exception ex) { closeConnectionDialog(); UiUtils.sDisplayHelp( MainActivity.this, R.string.error, R.string.error_unknown ); } protected void closeConnectionDialog() { mViewHolder.connectionIcon.setVisibility(View.VISIBLE); mConnectionDialog.dismiss(); mConnectionDialog = null; } } // ============================================================ // =========== KEYBOARD APPEAR LISTENER // ============================================================ class OnGlobalLayoutListenerImpl implements ViewTreeObserver.OnGlobalLayoutListener { @Override public void onGlobalLayout() { int heightDiff = mViewHolder.activityRootView.getRootView().getHeight() - mViewHolder.activityRootView.getHeight(); if ((heightDiff > getMinKeyboardHeight()) != mIsKeyboardDisplayed) { mIsKeyboardDisplayed = heightDiff > getMinKeyboardHeight(); setElementVisibilityForSearch(mIsKeyboardDisplayed); } } } protected int getMinKeyboardHeight() { return (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, MIN_KEYBOARD_HEIGHT_IN_DP, getResources().getDisplayMetrics() ); } // ============================================================ // =========== BILLING // ============================================================ class AbstractBillingObserverImpl extends AbstractBillingObserver { public AbstractBillingObserverImpl(Activity activity) { super(activity); } public void onBillingChecked(boolean supported) { mIsBillingSupported = supported; if (supported) { restoreTransactions(); } } public void onPurchaseStateChanged(String itemId, Transaction.PurchaseState state) { } public void onRequestPurchaseResponse(String itemId, BillingRequest.ResponseCode response) { } public void onSubscriptionChecked(boolean supported) { } } /** * Restores previous transactions, if any. This happens if the application * has just been installed or the user wiped data. We do not want to do this * on every startup, rather, we want to do only when the database needs to * be initialized. */ protected void restoreTransactions() { if (!mBillingObserver.isTransactionsRestored()) { BillingController.restoreTransactions(this); } } protected Dialog createBillingNotSupportedDialog(int titleId, int messageId) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(titleId) .setIcon(android.R.drawable.stat_sys_warning) .setMessage(messageId) .setCancelable(false) .setPositiveButton(android.R.string.ok, null) ; return builder.create(); } // =================================================== // =========== KEY EVENT HANDLERS // =================================================== @Override public boolean dispatchKeyEvent(KeyEvent event) { switch (event.getKeyCode()) { case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_VOLUME_DOWN: if (mAimpPlayer.isConnected()) { int direction = (event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_UP) ? VolumeUpdater.DIRECTION_UP : VolumeUpdater.DIRECTION_DOWN; if (event.getAction() == KeyEvent.ACTION_DOWN) { tVolumeUpdater.start(mAimpPlayer, direction); } else { tVolumeUpdater.stop(mAimpPlayer); } } return true; default: return super.dispatchKeyEvent(event); } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_MENU) { onMenuKeyPressed(); return true; } else { return super.onKeyDown(keyCode, event); } } // =================================================== // ===================== MENU // =================================================== protected void onMenuKeyPressed() { showPopupDialog(); } protected void showPopupDialog() { if (mMenuDialog != null) { mMenuDialog.dismiss(); } else { int[] items = new int[] { mAimpPlayer.isConnected() ? PlayerMenu.TYPE_DISCONNECT : PlayerMenu.TYPE_CONNECT, PlayerMenu.TYPE_INFO, PlayerMenu.TYPE_PREFERENCES, PlayerMenu.TYPE_DONATE, PlayerMenu.TYPE_EXIT }; mMenuDialog = PlayerMenu.createDialog(this, items, mMenuListener); mMenuDialog.setOnDismissListener(new DialogInterface.OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) { mMenuDialog = null; } }); mMenuDialog.show(); } } class PlayerMenuChooseImpl implements PlayerMenu.OnChooseListener { @Override public void onChoose(int type) { switch (type) { case PlayerMenu.TYPE_CONNECT: doConnect(); break; case PlayerMenu.TYPE_DISCONNECT: doDisconnect(); break; case PlayerMenu.TYPE_INFO: displayInfoList(); break; case PlayerMenu.TYPE_DONATE: if (!mIsBillingSupported) { createBillingNotSupportedDialog(R.string.billing_not_supported_title, R.string.billing_not_supported_message).show(); } else { BillingController.requestPurchase(MainActivity.this, Billing.DONATE_1_99, true /* confirm */, null); } break; case PlayerMenu.TYPE_PREFERENCES: startActivity(new Intent(getApplicationContext(), SettingsActivity.class)); break; case PlayerMenu.TYPE_EXIT: if (mAimpPlayer.isConnected()) { doDisconnect(); } finish(); break; default: break; } mMenuDialog = null; } }; protected void displayInfoList() { CharSequence[] items = new CharSequence[4]; items[0] = getResources().getString(R.string.menu_info_reference); items[1] = getResources().getString(R.string.menu_info_old_versions); items[2] = getResources().getString(R.string.menu_info_github); items[3] = getResources().getString(R.string.menu_info_about); mMenuDialog = new AlertDialog.Builder(this) .setTitle(R.string.menu_info) .setItems(items, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int item) { if (item == 0) { UiUtils.sDisplayHelp(MainActivity.this, R.string.info_reference_title, R.string.info_reference_content); } if (item == 1) { showOldVersionsDialog(); } if (item == 2) { showGitHubDialog(); } if (item == 3) { UiUtils.sDisplayHelp(MainActivity.this, R.string.info_about_title, R.string.info_about_content); } } }) .setNeutralButton(R.string.close, null) .create() ; mMenuDialog.show(); } protected void showOldVersionsDialog() { Dialog d = new AlertDialog.Builder(this) .setTitle(R.string.info_old_versions_title) .setMessage(R.string.info_old_versions_message) .setPositiveButton(R.string.info_old_versions_goto_catalog_title, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Intent i = new Intent( Intent.ACTION_VIEW, Uri.parse(getString(R.string.info_old_versions_goto_catalog_message)) ); startActivity(i); } }) .setNeutralButton(R.string.cancel, null) .create(); d.show(); } protected void showGitHubDialog() { Dialog d = new AlertDialog.Builder(this) .setTitle(R.string.menu_info_github_title) .setMessage(R.string.menu_info_github_message) .setPositiveButton(R.string.menu_info_github_goto_catalog_title, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Intent i = new Intent( Intent.ACTION_VIEW, Uri.parse(getString(R.string.menu_info_github_goto_catalog_message)) ); startActivity(i); } }) .setNeutralButton(R.string.cancel, null) .create(); d.show(); } protected Dialog createConnectionErrorDialog(int titleResId, int messageResId) { AlertDialog alertDialog = new AlertDialog.Builder(this) .setTitle(titleResId) .setMessage(messageResId) .setPositiveButton( R.string.goto_settings, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { startActivity(new Intent(getApplicationContext(), SettingsActivity.class)); } } ) .setNegativeButton( R.string.close, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { } } ) .create() ; return alertDialog; } }