<string name="context_menu_remove_from_recent">Remove from recent</string>
<string name="context_menu_use_as_ringtone">Use as ringtone</string>
<string name="context_menu_remove_from_playlist">Remove from playlist</string>
+ <string name="context_menu_change_image">Change image</string>
<!-- Content descriptions -->
<string name="accessibility_play">Play</string>
package com.cyngn.eleven.cache;
+import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
import android.widget.ImageView;
-
import com.cyngn.eleven.Config;
import com.cyngn.eleven.MusicPlaybackService;
import com.cyngn.eleven.cache.PlaylistWorkerTask.PlaylistWorkerType;
import com.cyngn.eleven.utils.MusicUtils;
import com.cyngn.eleven.widgets.BlurScrimImage;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
/**
* A subclass of {@link ImageWorker} that fetches images from a URL.
*/
public class ImageFetcher extends ImageWorker {
+ private static final int DEFAULT_MAX_IMAGE_HEIGHT = 1024;
+
+ private static final int DEFAULT_MAX_IMAGE_WIDTH = 1024;
+
private static ImageFetcher sInstance = null;
/**
.append(Config.ALBUM_ART_SUFFIX)
.toString();
}
+
+ /**
+ * Decode and sample down a {@link Bitmap} from a Uri.
+ *
+ * @param selectedImage Uri of the Image to decode
+ * @return A {@link Bitmap} sampled down from the original with the same
+ * aspect ratio and dimensions that are equal to or greater than the
+ * requested width and height
+ */
+ public static Bitmap decodeSampledBitmapFromUri(ContentResolver cr, final Uri selectedImage) {
+ // First decode with inJustDecodeBounds=true to check dimensions
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+
+ try {
+ InputStream input = cr.openInputStream(selectedImage);
+ BitmapFactory.decodeStream(input, null, options);
+ input.close();
+
+ if (options.outHeight == -1 || options.outWidth == -1) {
+ return null;
+ }
+
+ // Calculate inSampleSize
+ options.inSampleSize = calculateInSampleSize(options, DEFAULT_MAX_IMAGE_WIDTH,
+ DEFAULT_MAX_IMAGE_HEIGHT);
+
+ // Decode bitmap with inSampleSize set
+ options.inJustDecodeBounds = false;
+ input = cr.openInputStream(selectedImage);
+ return BitmapFactory.decodeStream(input, null, options);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ return null;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ /**
+ * Calculate an inSampleSize for use in a
+ * {@link android.graphics.BitmapFactory.Options} object when decoding
+ * bitmaps using the decode* methods from {@link BitmapFactory}. This
+ * implementation calculates the closest inSampleSize that will result in
+ * the final decoded bitmap having a width and height equal to or larger
+ * than the requested width and height. This implementation does not ensure
+ * a power of 2 is returned for inSampleSize which can be faster when
+ * decoding but results in a larger bitmap which isn't as useful for caching
+ * purposes.
+ *
+ * @param options An options object with out* params already populated (run
+ * through a decode* method with inJustDecodeBounds==true
+ * @param reqWidth The requested width of the resulting bitmap
+ * @param reqHeight The requested height of the resulting bitmap
+ * @return The value to be used for inSampleSize
+ */
+ public static final int calculateInSampleSize(final BitmapFactory.Options options,
+ final int reqWidth, final int reqHeight) {
+ /* Raw height and width of image */
+ final int height = options.outHeight;
+ final int width = options.outWidth;
+ int inSampleSize = 1;
+
+ if (height > reqHeight || width > reqWidth) {
+ if (width > height) {
+ inSampleSize = Math.round((float)height / (float)reqHeight);
+ } else {
+ inSampleSize = Math.round((float)width / (float)reqWidth);
+ }
+
+ // This offers some additional logic in case the image has a strange
+ // aspect ratio. For example, a panorama may have a much larger
+ // width than height. In these cases the total pixels might still
+ // end up being too large to fit comfortably in memory, so we should
+ // be more aggressive with sample down the image (=larger
+ // inSampleSize).
+
+ final float totalPixels = width * height;
+
+ /* More than 2x the requested pixels we'll sample down further */
+ final float totalReqPixelsCap = reqWidth * reqHeight * 2;
+
+ while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
+ inSampleSize++;
+ }
+ }
+ return inSampleSize;
+ }
}
int DELETE = 120; // delete track from device
int NEW_PLAYLIST = 130; // create new playlist - also in res/menu!
int PLAYLIST_SELECTED = 140; // this is used for existing playlists
+ int CHANGE_IMAGE = 150; // set new art for artist/album
// not currently in use
int FETCH_ARTIST_IMAGE = 200;
--- /dev/null
+/*
+ * Copyright (C) 2012 Andrew Neal 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.cyngn.eleven.menu;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.widget.ArrayAdapter;
+import android.widget.ListAdapter;
+import android.widget.Toast;
+import com.cyngn.eleven.Config;
+import com.cyngn.eleven.R;
+import com.cyngn.eleven.ui.activities.HomeActivity;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.Lists;
+import com.cyngn.eleven.utils.MusicUtils;
+
+import java.util.ArrayList;
+
+/**
+ * Used when the user requests to modify Album art or Artist image
+ * It provides an easy interface for them to choose a new image, use the old
+ * image, or search Google for one.
+ *
+ * @author Andrew Neal (andrewdneal@gmail.com)
+ */
+public class PhotoSelectionDialog extends DialogFragment {
+
+ private static final int NEW_PHOTO = 0;
+
+ private static final int OLD_PHOTO = 1;
+
+ private final ArrayList<String> mChoices = Lists.newArrayList();
+
+ private static ProfileType mProfileType;
+
+ private String mKey;
+
+ /**
+ * Empty constructor as per the {@link Fragment} documentation
+ */
+ public PhotoSelectionDialog() {
+ }
+
+ /**
+ * @param title The dialog title.
+ * @param type Either Artist or Album
+ * @param key key to query ImageFetcher
+ * @return A new instance of the dialog.
+ */
+ public static PhotoSelectionDialog newInstance(final String title, final ProfileType type,
+ String key) {
+ final PhotoSelectionDialog frag = new PhotoSelectionDialog();
+ final Bundle args = new Bundle();
+ args.putString(Config.NAME, title);
+ frag.setArguments(args);
+ mProfileType = type;
+ frag.mKey = key;
+ return frag;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Dialog onCreateDialog(final Bundle savedInstanceState) {
+ final String title = getArguments().getString(Config.NAME);
+ switch (mProfileType) {
+ case ARTIST:
+ setArtistChoices();
+ break;
+ case ALBUM:
+ setAlbumChoices();
+ break;
+ case OTHER:
+ setOtherChoices();
+ break;
+ default:
+ break;
+ }
+ // Dialog item Adapter
+ final HomeActivity activity = (HomeActivity) getActivity();
+ final ListAdapter adapter = new ArrayAdapter<String>(activity,
+ android.R.layout.select_dialog_item, mChoices);
+ return new AlertDialog.Builder(activity).setTitle(title)
+ .setAdapter(adapter, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ switch (which) {
+ case NEW_PHOTO:
+ activity.selectNewPhoto(mKey);
+ break;
+ case OLD_PHOTO:
+ MusicUtils.selectOldPhoto(activity, mKey);
+ break;
+ default:
+ break;
+ }
+ }
+ }).create();
+ }
+
+ /**
+ * Adds the choices for the artist profile image.
+ */
+ private void setArtistChoices() {
+ // Select a photo from the gallery
+ mChoices.add(NEW_PHOTO, getString(R.string.new_photo));
+ if (ApolloUtils.isOnline(getActivity())) {
+ // Option to fetch the old artist image
+ mChoices.add(OLD_PHOTO, getString(R.string.context_menu_fetch_artist_image));
+ }
+ }
+
+ /**
+ * Adds the choices for the album profile image.
+ */
+ private void setAlbumChoices() {
+ // Select a photo from the gallery
+ mChoices.add(NEW_PHOTO, getString(R.string.new_photo));
+ // Option to fetch the old album image
+ if (ApolloUtils.isOnline(getActivity())) {
+ // Option to fetch the old artist image
+ mChoices.add(OLD_PHOTO, getString(R.string.context_menu_fetch_album_art));
+ }
+ }
+
+ /**
+ * Adds the choices for the genre and playlist images.
+ */
+ private void setOtherChoices() {
+ // Select a photo from the gallery
+ mChoices.add(NEW_PHOTO, getString(R.string.new_photo));
+ // Option to use the default image
+ mChoices.add(OLD_PHOTO, getString(R.string.use_default));
+ }
+
+ /**
+ * Easily detect the MIME type
+ */
+ public enum ProfileType {
+ ARTIST, ALBUM, ProfileType, OTHER
+ }
+}
package com.cyngn.eleven.ui.activities;
import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
-
+import android.text.TextUtils;
import com.cyngn.eleven.Config;
import com.cyngn.eleven.R;
+import com.cyngn.eleven.cache.ImageFetcher;
import com.cyngn.eleven.ui.fragments.AlbumDetailFragment;
import com.cyngn.eleven.ui.fragments.ArtistDetailFragment;
import com.cyngn.eleven.ui.fragments.ISetupActionBar;
import com.cyngn.eleven.ui.fragments.phone.MusicBrowserPhoneFragment;
import com.cyngn.eleven.ui.fragments.profile.LastAddedFragment;
import com.cyngn.eleven.ui.fragments.profile.TopTracksFragment;
+import com.cyngn.eleven.utils.ApolloUtils;
+import com.cyngn.eleven.utils.MusicUtils;
public class HomeActivity extends SlidingPanelActivity {
private static final String ACTION_PREFIX = HomeActivity.class.getName();
public static final String ACTION_VIEW_PLAYLIST_DETAILS = ACTION_PREFIX + ".view.PlaylistDetails";
public static final String ACTION_VIEW_SMART_PLAYLIST = ACTION_PREFIX + ".view.SmartPlaylist";
+ private static final int NEW_PHOTO = 1;
+
+ private String mKey;
private boolean mLoadedBaseFragment = false;
private Handler mHandler = new Handler();
}
}
}
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == NEW_PHOTO && !TextUtils.isEmpty(mKey)) {
+ if (resultCode == RESULT_OK) {
+ MusicUtils.removeFromCache(this, mKey);
+ final Uri selectedImage = data.getData();
+ Bitmap bitmap = ImageFetcher.decodeSampledBitmapFromUri(getContentResolver(),
+ selectedImage);
+
+ ImageFetcher imageFetcher = ApolloUtils.getImageFetcher(this);
+ imageFetcher.addBitmapToCache(mKey, bitmap);
+
+ MusicUtils.refresh();
+ }
+ }
+ }
+
+ /**
+ * Starts an activity for result that returns an image from the Gallery.
+ */
+ public void selectNewPhoto(String key) {
+ mKey = key;
+ // Now open the gallery
+ final Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
+ intent.setType("image/*");
+ startActivityForResult(intent, NEW_PHOTO);
+ }
}
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
-
import com.cyngn.eleven.Config;
import com.cyngn.eleven.R;
import com.cyngn.eleven.adapters.AlbumDetailSongAdapter;
import com.cyngn.eleven.cache.ImageFetcher;
import com.cyngn.eleven.model.Album;
import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.ui.activities.BaseActivity;
import com.cyngn.eleven.utils.AlbumPopupMenuHelper;
import com.cyngn.eleven.utils.GenreFetcher;
import com.cyngn.eleven.utils.MusicUtils;
private DetailSongAdapter mSongAdapter;
private TextView mAlbumDuration;
private TextView mGenre;
+ private ImageView mAlbumArt;
private PopupMenuHelper mSongMenuHelper;
private long mAlbumId;
private String mArtistName;
String year = arguments.getString(Config.ALBUM_YEAR);
int songCount = arguments.getInt(Config.SONG_COUNT);
- ImageView albumArt = (ImageView)mRootView.findViewById(R.id.album_art);
- albumArt.setContentDescription(mAlbumName);
- ImageFetcher.getInstance(getActivity()).loadAlbumImage(artist, mAlbumName, mAlbumId, albumArt);
+ mAlbumArt = (ImageView)mRootView.findViewById(R.id.album_art);
+ mAlbumArt.setContentDescription(mAlbumName);
+ ImageFetcher.getInstance(getActivity()).loadAlbumImage(artist, mAlbumName, mAlbumId, mAlbumArt);
TextView title = (TextView)mRootView.findViewById(R.id.title);
title.setText(mAlbumName);
@Override
public void restartLoader() {
getLoaderManager().restartLoader(LOADER_ID, getArguments(), mSongAdapter);
+ ImageFetcher.getInstance(getActivity()).loadAlbumImage(mArtistName, mAlbumName, mAlbumId,
+ mAlbumArt);
}
@Override
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ListView;
-
import com.cyngn.eleven.Config;
import com.cyngn.eleven.R;
import com.cyngn.eleven.adapters.ArtistDetailAlbumAdapter;
import com.cyngn.eleven.model.Album;
import com.cyngn.eleven.model.Artist;
import com.cyngn.eleven.model.Song;
+import com.cyngn.eleven.ui.activities.BaseActivity;
import com.cyngn.eleven.utils.AlbumPopupMenuHelper;
import com.cyngn.eleven.utils.ArtistPopupMenuHelper;
import com.cyngn.eleven.utils.MusicUtils;
LoaderManager lm = getLoaderManager();
lm.restartLoader(ALBUM_LOADER_ID, arguments, mAlbumAdapter);
lm.restartLoader(SONG_LOADER_ID, arguments, mSongAdapter);
+
+ ImageFetcher.getInstance(getActivity()).loadArtistImage(mArtistName, mHero);
}
@Override
return mViewPager.getCurrentItem() == MusicFragments.ARTIST.ordinal();
}
- private ArtistFragment getArtistFragment() {
+ public ArtistFragment getArtistFragment() {
return (ArtistFragment)mPagerAdapter.getFragment(MusicFragments.ARTIST.ordinal());
}
return mViewPager.getCurrentItem() == MusicFragments.ALBUM.ordinal();
}
- private AlbumFragment getAlbumFragment() {
+ public AlbumFragment getAlbumFragment() {
return (AlbumFragment)mPagerAdapter.getFragment(MusicFragments.ALBUM.ordinal());
}
return mViewPager.getCurrentItem() == MusicFragments.SONG.ordinal();
}
- private SongFragment getSongFragment() {
+ public SongFragment getSongFragment() {
return (SongFragment)mPagerAdapter.getFragment(MusicFragments.SONG.ordinal());
}
import android.app.Activity;
import android.support.v4.app.FragmentManager;
+import android.view.MenuItem;
import com.cyngn.eleven.Config;
import com.cyngn.eleven.cache.ImageFetcher;
import com.cyngn.eleven.menu.DeleteDialog;
+import com.cyngn.eleven.menu.FragmentMenuItems;
+import com.cyngn.eleven.menu.PhotoSelectionDialog;
import com.cyngn.eleven.model.Album;
public abstract class AlbumPopupMenuHelper extends PopupMenuHelper {
protected String getArtistName() {
return mAlbum.mArtistName;
}
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ boolean handled = super.onMenuItemClick(item);
+ if (!handled && item.getGroupId() == getGroupId()) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.CHANGE_IMAGE:
+ String key = ImageFetcher.generateAlbumCacheKey(mAlbum.mAlbumName,
+ getArtistName());
+ PhotoSelectionDialog.newInstance(mAlbum.mAlbumName,
+ PhotoSelectionDialog.ProfileType.ALBUM, key)
+ .show(mFragmentManager, "PhotoSelectionDialog");
+ return true;
+ }
+ }
+
+ return handled;
+ }
}
import android.app.Activity;
import android.support.v4.app.FragmentManager;
+import android.view.MenuItem;
import com.cyngn.eleven.Config;
import com.cyngn.eleven.menu.DeleteDialog;
+import com.cyngn.eleven.menu.FragmentMenuItems;
+import com.cyngn.eleven.menu.PhotoSelectionDialog;
import com.cyngn.eleven.model.Artist;
public abstract class ArtistPopupMenuHelper extends PopupMenuHelper {
DeleteDialog.newInstance(artist, getIdList(), artist)
.show(mFragmentManager, "DeleteDialog");
}
+
+ @Override
+ protected String getArtistName() {
+ return mArtist.mArtistName;
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ boolean handled = super.onMenuItemClick(item);
+ if (!handled && item.getGroupId() == getGroupId()) {
+ switch (item.getItemId()) {
+ case FragmentMenuItems.CHANGE_IMAGE:
+ PhotoSelectionDialog.newInstance(getArtistName(),
+ PhotoSelectionDialog.ProfileType.ARTIST, getArtistName())
+ .show(mFragmentManager, "PhotoSelectionDialog");
+ return true;
+ }
+ }
+
+ return handled;
+ }
}
\ No newline at end of file
import android.net.Uri;
import android.os.IBinder;
import android.os.RemoteException;
+import android.os.SystemClock;
import android.provider.BaseColumns;
import android.provider.MediaStore;
import android.provider.MediaStore.Audio.AlbumColumns;
import com.cyngn.eleven.IElevenService;
import com.cyngn.eleven.MusicPlaybackService;
import com.cyngn.eleven.R;
+import com.cyngn.eleven.cache.ImageFetcher;
import com.cyngn.eleven.loaders.LastAddedLoader;
import com.cyngn.eleven.loaders.PlaylistLoader;
import com.cyngn.eleven.loaders.PlaylistSongLoader;
}
return true;
}
+
+ /**
+ * Removes the header image from the cache.
+ */
+ public static void removeFromCache(Activity activity, String key) {
+ ImageFetcher imageFetcher = ApolloUtils.getImageFetcher(activity);
+ imageFetcher.removeFromCache(key);
+ // Give the disk cache a little time before requesting a new image.
+ SystemClock.sleep(80);
+ }
+
+ /**
+ * Removes image from cache so that the stock image is retrieved on reload
+ */
+ public static void selectOldPhoto(Activity activity, String key) {
+ // First remove the old image
+ removeFromCache(activity, key);
+ MusicUtils.refresh();
+ }
}
import android.view.MenuItem;
import android.view.View;
import android.widget.PopupMenu;
-
+import android.widget.Toast;
import com.android.internal.view.menu.ContextMenuBuilder;
import com.android.internal.view.menu.MenuBuilder;
import com.cyngn.eleven.Config;
import com.cyngn.eleven.R;
import com.cyngn.eleven.menu.CreateNewPlaylist;
import com.cyngn.eleven.menu.FragmentMenuItems;
+import com.cyngn.eleven.menu.PhotoSelectionDialog;
import com.cyngn.eleven.menu.RenamePlaylist;
import com.cyngn.eleven.provider.RecentStore;
FragmentMenuItems.ADD_TO_QUEUE,
FragmentMenuItems.ADD_TO_PLAYLIST,
FragmentMenuItems.DELETE,
+ FragmentMenuItems.CHANGE_IMAGE,
};
case Album:
return new int[] {
FragmentMenuItems.ADD_TO_PLAYLIST,
FragmentMenuItems.MORE_BY_ARTIST,
FragmentMenuItems.DELETE,
+ FragmentMenuItems.CHANGE_IMAGE,
};
case Song:
return new int[] {
return R.string.remove_from_queue;
case FragmentMenuItems.PLAY_NEXT:
return R.string.context_menu_play_next;
+ case FragmentMenuItems.CHANGE_IMAGE:
+ return R.string.context_menu_change_image;
}
return 0;