/** * Copyright © 2011,2013 Konstantin Livitski * * This file is part of n-Puzzle application. n-Puzzle application 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 3 of the License, or (at your option) any later version. * * n-Puzzle application contains adaptations of artwork covered by the Creative * Commons Attribution-ShareAlike 3.0 Unported license. Please refer to the * NOTICE.md file at the root of this distribution or repository for licensing * terms that apply to that artwork. * * n-Puzzle application 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 * n-Puzzle application; if not, see the LICENSE/gpl.txt file of this distribution * or visit <http://www.gnu.org/licenses>. */ package name.livitski.games.puzzle.android; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.StringTokenizer; import name.livitski.games.puzzle.android.model.Game; import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.util.Log; import android.view.Display; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.BaseAdapter; import android.widget.GridView; import android.widget.AdapterView; import android.widget.TextView; /** * User's interface to image selector page. */ public class ImageSelection extends Activity implements AdapterView.OnItemClickListener { public ImageSelection() { super(R.layout.images); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); initUserImageSuffixes(); GridView grid = (GridView)findViewById(R.id.image_selection_grid); grid.setOnItemClickListener(this); if (null == imageSource) imageSource = new ImageSource(); imageSource.setContext(this); imageSource.init(); updateContentView(); } public void onItemClick(AdapterView<?> parent, View view, int position, long id) { final ImageSource.ImageWithConstraints imageInfo = (ImageSource.ImageWithConstraints)parent.getItemAtPosition(position); final Serializable imageId = imageInfo.getImageId(); if (deletionMode) { deleteUserImage((String)imageId); deletionMode = false; updateContentView(); } else { final Game.Level initialComplexity = imageInfo.getInitialComplexity(); final Intent data = new Intent(); data.putExtra(EXTRA_SELECTED_IMAGE_ID_KEY, imageId); if (null != initialComplexity) data.putExtra(EXTRA_SELECTED_IMAGE_INITIAL_LEVEL, initialComplexity); setResult(RESULT_OK, data); if (imageId instanceof String) markUserImageSelected((String)imageId); finish(); } } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.pics_menu, menu); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { return !deletionMode; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.item_add_picture: selectPictureToAdd(); break; case R.id.item_delete_picture: deletionMode = true; updateContentView(); break; default: return super.onOptionsItemSelected(item); } return true; } @Override public void onBackPressed() { if (deletionMode) { deletionMode = false; updateContentView(); } else super.onBackPressed(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (ACTIVITY_ADD_PICTURE == requestCode && RESULT_OK == resultCode) { final Uri imageURI = data.getData(); if (null != imageAdder) { String msg = getResources().getString(R.string.image_load_error); Log.e(LOG_TAG, "Another image add operation is in progress"); alert(msg); } final Handler handler = progress(getResources().getString(R.string.image_load_progress), null); final Display display = getWindowManager().getDefaultDisplay(); int maxDisplaySize = display.getWidth(); if (maxDisplaySize < display.getHeight()) maxDisplaySize = display.getHeight(); imageAdder = new ImageConverter(this, handler) { @Override public void run() { InputStream pictureData = null; OutputStream cacheOutput = null; String userImageId = null; try { BitmapFactory.Options bitmapInfo = prepareOptions(); pictureData = openInputStream(); synchronized(imageSource) { userImageId = allocateUserImage(); cacheOutput = new FileOutputStream(ImageSource.imageFileForId(userImageId, ImageSelection.this)); if (null != bitmapInfo) { // downsample and save final Bitmap bitmap = BitmapFactory.decodeStream(pictureData, null, bitmapInfo); bitmap.compress(CompressFormat.JPEG, 85, cacheOutput); } else { // copy image as-is byte buffer[] = new byte[16384]; for (int read; (read = pictureData.read(buffer)) >= 0;) cacheOutput.write(buffer, 0, read); } cacheOutput.close(); cacheOutput = null; } } catch (Throwable failure) { setStatus(failure); } finally { if (null != pictureData) try { pictureData.close(); } catch (Exception ignored) {} if (null != cacheOutput) try { cacheOutput.close(); } catch (Exception ignored) {} if (null != userImageId) try { imageSource.onImageUpdate(userImageId); } catch (Exception failure) { Log.e(LOG_TAG, "Thumbnail update failed for " + userImageId, failure); if (null == getStatus()) setStatus(failure); } getParentHandler().sendEmptyMessage(0); } } }; imageAdder.setImageURI(imageURI); imageAdder.setMaxFrameDimension(maxDisplaySize); submitBackgroundTask(imageAdder); } } @Override protected void onPause() { super.onPause(); saveSettings(); } @Override protected void onCompletion() { if (null != imageAdder) { if (null != imageAdder.getStatus()) { String msg = getResources().getString(R.string.image_load_error); Log.e(LOG_TAG, msg, imageAdder.getStatus()); alert(msg); } imageSource.notifyDataSetChanged(); imageAdder = null; } } protected void updateContentView() { final View promptView = findViewById(R.id.image_selection_prompt_comment); if (deletionMode) { ((TextView)findViewById(R.id.image_selection_prompt)).setText(R.string.image_prompt_delete); final BaseAdapter source = imageSource.userImageSource(); if (0 == source.getCount()) { ((TextView)promptView).setText(R.string.image_nothing_to_delete_comment); promptView.setVisibility(View.VISIBLE); } else promptView.setVisibility(View.INVISIBLE); final GridView grid = (GridView)findViewById(R.id.image_selection_grid); grid.setAdapter(source); } else { ((TextView)findViewById(R.id.image_selection_prompt)).setText(R.string.image_prompt); ((TextView)promptView).setText(R.string.image_prompt_comment); promptView.setVisibility(View.VISIBLE); GridView grid = (GridView)findViewById(R.id.image_selection_grid); grid.setAdapter(imageSource); } } protected void selectPictureToAdd() { Intent request = new Intent(); request.setAction(Intent.ACTION_PICK); request.setType(MIME_TYPE_IMAGE); try { startActivityForResult(request, ACTIVITY_ADD_PICTURE); } catch (ActivityNotFoundException noManager) { String msg = getResources().getString(R.string.no_gallery_error); Log.e(LOG_TAG, msg, noManager); alert(msg); } } protected void deleteUserImage(final String id) { final File file = ImageSource.imageFileForId(id, this); final String suffix = extractUserImageSuffix(id); synchronized (imageSource) { if (file.exists() && !file.delete()) { String msg = getResources().getString(R.string.image_delete_error); Log.e(LOG_TAG, msg + ", file: " + file); alert(msg); } else { synchronized (userImageSuffixesLRU) { userImageSuffixesLRU.remove(suffix); userImageSuffixesLRU.add(0, suffix); } } imageSource.updateUserImages(); } } /** * Allocates a cache file name for the new user image and * returns it. Removes the last recently used file from cache * if necessary. */ protected String allocateUserImage() { final String index; synchronized (userImageSuffixesLRU) { index = userImageSuffixesLRU.remove(0); userImageSuffixesLRU.add(index); } final String name = ImageSource.IMAGE_FILE_PREFIX + index + ImageSource.IMAGE_FILE_SUFFIX; final File file = ImageSource.imageFileForId(name, this); if (file.exists()) file.delete(); return name; } /** * Moves the suffix of recently used file to the end of * the least recently used queue. */ protected void markUserImageSelected(final String name) { final String index = extractUserImageSuffix(name); synchronized (userImageSuffixesLRU) { userImageSuffixesLRU.remove(index); userImageSuffixesLRU.add(index); } } protected String extractUserImageSuffix(final String name) { if (!name.startsWith(ImageSource.IMAGE_FILE_PREFIX) || !name.endsWith(ImageSource.IMAGE_FILE_SUFFIX)) throw new IllegalArgumentException("File name \"" + name + "\" is not valid for the internal copy of a user's image."); final String index = name.substring(ImageSource.IMAGE_FILE_PREFIX.length(), name.length() - ImageSource.IMAGE_FILE_SUFFIX.length()); return index; } protected void saveSettings() { SharedPreferences.Editor settings = getPreferences(MODE_PRIVATE).edit(); saveUserImageSuffixes(settings); settings.commit(); } protected void initUserImageSuffixes() { SharedPreferences preferences = getPreferences(MODE_PRIVATE); String lruString = preferences.getString(SETTING_USER_IMAGE_LRU, null); Set<String> unusedSuffixes = new HashSet<String>(ImageSource.MAX_USER_IMAGE_COUNT, 1f); for (int i = 0; ImageSource.MAX_USER_IMAGE_COUNT > i; i++) unusedSuffixes.add(Integer.toString(i)); userImageSuffixesLRU = new LinkedList<String>(); synchronized (userImageSuffixesLRU) { if (null != lruString) { for ( StringTokenizer suffixesLRU = new StringTokenizer(lruString, DELMINTER_LRU_STRING); suffixesLRU.hasMoreTokens(); ) { String suffix = suffixesLRU.nextToken(); userImageSuffixesLRU.add(suffix); unusedSuffixes.remove(suffix); } } userImageSuffixesLRU.addAll(0, unusedSuffixes); } } protected void saveUserImageSuffixes(SharedPreferences.Editor settings) { synchronized (userImageSuffixesLRU) { if (userImageSuffixesLRU.isEmpty()) settings.remove(SETTING_USER_IMAGE_LRU); else { StringBuilder lruString = new StringBuilder(); for (Iterator<String> it = userImageSuffixesLRU.iterator();;) { String index = it.next(); lruString.append(index); if (it.hasNext()) lruString.append(DELMINTER_LRU_STRING); else break; } settings.putString(SETTING_USER_IMAGE_LRU, lruString.toString()); } } } protected static final String EXTRA_SELECTED_IMAGE_ID_KEY = "selected_image_id"; protected static final String EXTRA_SELECTED_IMAGE_INITIAL_LEVEL = "selected_image_initial_level"; protected static final String SETTING_USER_IMAGE_LRU = "user_image_lru"; protected static final String DELMINTER_LRU_STRING = ","; protected static final String MIME_TYPE_IMAGE = "image/*"; protected static final int ACTIVITY_ADD_PICTURE = 0; protected static final int DIALOG_ALERT = Integer.MAX_VALUE - 10; private static final String LOG_TAG = "ImageSelection"; private boolean deletionMode; private ImageConverter imageAdder; /** Note: synchronized access, synchronize AFTER {@link #imageSource}. */ private List<String> userImageSuffixesLRU; private static ImageSource imageSource; }