Java tutorial
/* * Copyright (c) 2011 Kurt Aaholst <kaaholst@gmail.com> * * 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 uk.org.ngo.squeezer.framework; import android.annotation.TargetApi; import android.app.ActivityManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.support.annotation.CallSuper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringDef; import android.support.v4.app.NavUtils; import android.support.v4.app.TaskStackBuilder; import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBarActivity; import android.util.Log; import android.util.TypedValue; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.widget.Toast; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import uk.org.ngo.squeezer.HomeActivity; import uk.org.ngo.squeezer.Preferences; import uk.org.ngo.squeezer.R; import uk.org.ngo.squeezer.VolumePanel; import uk.org.ngo.squeezer.model.Player; import uk.org.ngo.squeezer.model.PlayerState; import uk.org.ngo.squeezer.service.ISqueezeService; import uk.org.ngo.squeezer.service.ServerString; import uk.org.ngo.squeezer.service.SqueezeService; import uk.org.ngo.squeezer.service.event.PlayerVolume; import uk.org.ngo.squeezer.util.ImageFetcher; import uk.org.ngo.squeezer.util.SqueezePlayer; import uk.org.ngo.squeezer.util.ThemeManager; /** * Common base class for all activities in Squeezer. * * @author Kurt Aaholst */ public abstract class BaseActivity extends ActionBarActivity implements HasUiThread { @Nullable private ISqueezeService mService = null; private final ThemeManager mTheme = new ThemeManager(); private int mThemeId = mTheme.getDefaultTheme().mThemeId; /** Records whether the activity has registered on the service's event bus. */ private boolean mRegisteredOnEventBus; private final Handler uiThreadHandler = new Handler() { }; private SqueezePlayer squeezePlayer; /** Option menu volume control entry. */ @Nullable private MenuItem mMenuItemVolume; /** Whether volume changes should be ignored. */ private boolean mIgnoreVolumeChange; /** Volume control panel. */ @Nullable private VolumePanel mVolumePanel; protected String getTag() { return getClass().getSimpleName(); } /** * @return The squeezeservice, or null if not bound */ @Nullable public ISqueezeService getService() { return mService; } public int getThemeId() { return mThemeId; } /** * Use this to post Runnables to work off thread */ @Override public Handler getUIThreadHandler() { return uiThreadHandler; } private final ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder binder) { mService = (ISqueezeService) binder; BaseActivity.this.onServiceConnected(mService); } @Override public void onServiceDisconnected(ComponentName name) { mService = null; } }; @Override protected void onCreate(android.os.Bundle savedInstanceState) { super.onCreate(savedInstanceState); mTheme.onCreate(this); ActionBar actionBar = getSupportActionBar(); actionBar.setIcon(R.drawable.ic_launcher); actionBar.setHomeButtonEnabled(true); bindService(new Intent(this, SqueezeService.class), serviceConnection, Context.BIND_AUTO_CREATE); Log.d(getTag(), "did bindService; serviceStub = " + getService()); } @Override public void setTheme(int resId) { super.setTheme(resId); mThemeId = resId; } @Override public void onResume() { super.onResume(); mTheme.onResume(this); if (mService != null) { maybeRegisterOnEventBus(mService); } mVolumePanel = new VolumePanel(this); // If SqueezePlayer is installed, start it // TODO Only when connected (or at least serveraddress is saved) if (SqueezePlayer.hasSqueezePlayer(this) && new Preferences(this).controlSqueezePlayer()) { squeezePlayer = new SqueezePlayer(this); } // Ensure that any image fetching tasks started by this activity do not finish prematurely. ImageFetcher.getInstance(this).setExitTasksEarly(false); } @Override public void onPause() { // At least some Samsung devices call onPause without ensuring that onResume is called // first, per https://code.google.com/p/android/issues/detail?id=74464, so mVolumePanel // may be null on those devices. if (mVolumePanel != null) { mVolumePanel.dismiss(); mVolumePanel = null; } if (squeezePlayer != null) { squeezePlayer.stopControllingSqueezePlayer(); squeezePlayer = null; } if (mRegisteredOnEventBus) { // If we are not bound to the service, it's process is no longer // running, so the callbacks are already cleaned up. if (mService != null) { mService.getEventBus().unregister(this); mService.cancelItemListRequests(this); mService.cancelSubscriptions(this); } mRegisteredOnEventBus = false; } // Ensure that any pending image fetching tasks are unpaused, and finish quickly. ImageFetcher imageFetcher = ImageFetcher.getInstance(this); imageFetcher.setExitTasksEarly(true); imageFetcher.setPauseWork(false); super.onPause(); } /** * Clear the image memory cache if memory gets low. */ @Override public void onLowMemory() { ImageFetcher.onLowMemory(); } @Override public void onDestroy() { super.onDestroy(); unbindService(serviceConnection); } /** Fix for https://code.google.com/p/android/issues/detail?id=63570. */ private boolean mIsRestoredToTop; @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); if ((intent.getFlags() | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) > 0) { mIsRestoredToTop = true; } } @Override @TargetApi(Build.VERSION_CODES.KITKAT) public void finish() { super.finish(); if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT && !isTaskRoot() && mIsRestoredToTop) { // 4.4.2 platform issues for FLAG_ACTIVITY_REORDER_TO_FRONT, // reordered activity back press will go to home unexpectedly, // Workaround: move reordered activity current task to front when it's finished. ActivityManager tasksManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE); tasksManager.moveTaskToFront(getTaskId(), ActivityManager.MOVE_TASK_NO_USER_ACTION); } } /** * Performs any actions necessary after the service has been connected. Derived classes * should call through to the base class. * <ul> * <li>Invalidates the options menu so that menu items can be adjusted based on * the state of the service connection.</li> * <li>Ensures that callbacks are registered.</li> * </ul> * * @param service The connection to the bound service. */ @CallSuper protected void onServiceConnected(@NonNull ISqueezeService service) { supportInvalidateOptionsMenu(); maybeRegisterOnEventBus(service); } /** * Conditionally registers with the service's EventBus. * <p> * Registration can happen in {@link #onResume()} and {@link * #onServiceConnected(uk.org.ngo.squeezer.service.ISqueezeService)}, this ensures that it only * happens once. * * @param service The connection to the bound service. */ private void maybeRegisterOnEventBus(@NonNull ISqueezeService service) { if (!mRegisteredOnEventBus) { service.getEventBus().registerSticky(this); mRegisteredOnEventBus = true; } } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.base_activity, menu); mMenuItemVolume = menu.findItem(R.id.menu_item_volume); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { boolean haveConnectedPlayers = isConnected() && mService != null && !mService.getConnectedPlayers().isEmpty(); if (mMenuItemVolume != null) { mMenuItemVolume.setVisible(haveConnectedPlayers); } return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: Intent upIntent = NavUtils.getParentActivityIntent(this); if (upIntent != null) { if (NavUtils.shouldUpRecreateTask(this, upIntent)) { TaskStackBuilder.create(this).addNextIntentWithParentStack(upIntent).startActivities(); } else { upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); NavUtils.navigateUpTo(this, upIntent); } } else { HomeActivity.show(this); } return true; case R.id.menu_item_volume: // Show the volume dialog. if (mService != null) { PlayerState playerState = mService.getPlayerState(); Player player = mService.getActivePlayer(); if (playerState != null && mVolumePanel != null) { mVolumePanel.postVolumeChanged(playerState.getCurrentVolume(), player == null ? "" : player.getName()); } return true; } } return super.onOptionsItemSelected(item); } /** * Block searches, when we are not connected. */ @Override public boolean onSearchRequested() { if (!isConnected()) { return false; } return super.onSearchRequested(); } /* * Intercept hardware volume control keys to control Squeezeserver * volume. * * Change the volume when the key is depressed. Suppress the keyUp * event, otherwise you get a notification beep as well as the volume * changing. */ @Override @CallSuper public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_UP: return changeVolumeBy(+5); case KeyEvent.KEYCODE_VOLUME_DOWN: return changeVolumeBy(-5); } return super.onKeyDown(keyCode, event); } @Override @CallSuper public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_VOLUME_DOWN: return true; } return super.onKeyUp(keyCode, event); } private boolean changeVolumeBy(int delta) { ISqueezeService service = getService(); if (service == null) { return false; } Log.v(getTag(), "Adjust volume by: " + delta); service.adjustVolumeBy(delta); return true; } public void onEvent(PlayerVolume event) { if (!mIgnoreVolumeChange && mVolumePanel != null && event.player == mService.getActivePlayer()) { mVolumePanel.postVolumeChanged(event.volume, event.player.getName()); } } public void setIgnoreVolumeChange(boolean ignoreVolumeChange) { mIgnoreVolumeChange = ignoreVolumeChange; } // Safe accessors public boolean canDownload() { return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD); } public boolean isConnected() { return mService != null && mService.isConnected(); } public String getServerString(ServerString stringToken) { return ServerString.values()[stringToken.ordinal()].getLocalizedString(); } // This section is just an easier way to call squeeze service public void play(PlaylistItem item) { playlistControl(PLAYLIST_PLAY_NOW, item, R.string.ITEM_PLAYING); } public void add(PlaylistItem item) { playlistControl(PLAYLIST_ADD_TO_END, item, R.string.ITEM_ADDED); } public void insert(PlaylistItem item) { playlistControl(PLAYLIST_PLAY_AFTER_CURRENT, item, R.string.ITEM_INSERTED); } private void playlistControl(@PlaylistControlCmd String cmd, PlaylistItem item, int resId) { if (mService == null) { return; } mService.playlistControl(cmd, item); Toast.makeText(this, getString(resId, item.getName()), Toast.LENGTH_SHORT).show(); } /** * Initiate download of songs for the supplied item. * * @param item Song or item with songs to download * @see ISqueezeService#downloadItem(FilterItem) */ public void downloadItem(FilterItem item) { if (canDownload()) mService.downloadItem(item); else Toast.makeText(this, R.string.DOWNLOAD_MANAGER_NEEDED, Toast.LENGTH_LONG).show(); } @StringDef({ PLAYLIST_PLAY_NOW, PLAYLIST_ADD_TO_END, PLAYLIST_PLAY_AFTER_CURRENT }) @Retention(RetentionPolicy.SOURCE) public @interface PlaylistControlCmd { } public static final String PLAYLIST_PLAY_NOW = "load"; public static final String PLAYLIST_ADD_TO_END = "add"; public static final String PLAYLIST_PLAY_AFTER_CURRENT = "insert"; /** * Look up an attribute resource styled for the current theme. * * @param attribute Attribute identifier to look up. * @return The resource identifier for the given attribute. */ public int getAttributeValue(int attribute) { TypedValue v = new TypedValue(); getTheme().resolveAttribute(attribute, v, true); return v.resourceId; } }