package de.vanitasvitae.sticktoalbum; import android.app.AlertDialog; import android.content.ContentUris; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; import android.media.MediaScannerConnection; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; import android.preference.PreferenceManager; import android.provider.MediaStore; import android.support.v7.app.ActionBarActivity; import android.text.InputType; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.webkit.MimeTypeMap; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.ListView; import android.widget.Toast; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; /** * "Stick" your photos and videos into albums by moving them into new folders and updating * Androids MediaStore. * @author vanitas */ public class MainActivity extends ActionBarActivity { private ArrayList sharedMedia; private static String Tag = "stickToAlbum"; SharedPreferences sharedPref = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); sharedPref = PreferenceManager.getDefaultSharedPreferences(this); if(sharedPref.getString(SettingsActivity.KEY_PREF_APP_PATH, "").isEmpty()) { resetAppDirectoryPath(); } sharedMedia = new ArrayList<>(); Intent intent = getIntent(); String action = intent.getAction(); String type = intent.getType(); Log.d(Tag, "Intent: " + action + ", " + type); if (Intent.ACTION_SEND.equals(action) && type != null) { if (type.startsWith("image/") || type.startsWith("video/")) { handleSendSingleFile(intent); } } else if (Intent.ACTION_SEND_MULTIPLE.equals(action) && type != null) { handleSendMultipleFiles(intent); } else { handleStartFromHomeScreen(); } } /** * Handle first run */ protected void onResume() { super.onResume(); if (sharedPref.getBoolean("firstrun", true)) { AlertDialog.Builder firstRunDialog = new AlertDialog.Builder(this); firstRunDialog.setTitle(R.string.dialog_title_first_run); firstRunDialog.setMessage(R.string.dialog_content_first_run); firstRunDialog.setPositiveButton(R.string.understood, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); firstRunDialog.setCancelable(false); firstRunDialog.show(); sharedPref.edit().putBoolean("firstrun", false).apply(); } } /** * Handle one single file being shared with the application. * Add the files Uri to the list of files to move and open the saveDialog. * @param intent Intent */ void handleSendSingleFile(Intent intent) { Uri imageUri = intent.getParcelableExtra(Intent.EXTRA_STREAM); if (imageUri != null) { String url = getRealPathFromURI(this, imageUri); if(url != null) { sharedMedia.add(url); openSaveDialog(); } else { handleUnsupportedMedia(); } } } /** * Handle multiple files being shared with the application. * Add the files Uris to the list of files to move and open the saveDialog. * @param intent Intent */ void handleSendMultipleFiles(Intent intent) { ArrayList imageUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); if (imageUris != null) { for(Uri u : imageUris) { String url = getRealPathFromURI(this, u); if(url != null) { sharedMedia.add(url); } } } if(imageUris == null || imageUris.size()==0) handleUnsupportedMedia(); else openSaveDialog(); } /** * Show Dialog to inform the user about the fact, that the feature he/she wants to use is not * implemented */ @SuppressWarnings("unused") void handleUnimplementedFeature() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.dialog_title_unimplemented_feature); builder.setMessage(R.string.dialog_content_unimplemented_feature); builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.cancel(); finish(); } }); builder.show(); } void handleUnsupportedMedia() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.dialog_title_unsupported_media); builder.setMessage(R.string.dialog_content_unsupported_media); builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.cancel(); finish(); } }); builder.show(); } void handleCouldNotWrite(String... urls) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.dialog_title_write_failed); String msg = getResources().getString(R.string.dialog_content_write_failed) + "\n\n"; for(String u : urls) msg = msg + u + "\n"; msg = msg.substring(0,msg.length()-1); builder.setMessage(msg); builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.cancel(); finish(); } }); builder.show(); } /** * Open a Dialog that prompts the user to enter an album name. * Reopens dialog, if album name is empty. * Closes app after moving files or when canceled. */ private void openSaveDialog() { final Context context = this; AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getResources().getText(R.string.dialog_title_album_name)); LayoutInflater inflater = this.getLayoutInflater(); View dialogView = inflater.inflate(R.layout.save_dialog, null); builder.setView(dialogView); final EditText input = (EditText) dialogView.findViewById(R.id.album_name); input.setInputType(InputType.TYPE_CLASS_TEXT); ListView existingAlbums = (ListView) dialogView.findViewById(R.id.album_list); final ArrayList directoryList = getFoldersOnDirectory(getAppDirectoryPath()); ArrayAdapter adapter = new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, directoryList); existingAlbums.setAdapter(adapter); existingAlbums.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { input.setText(directoryList.get(position)); } }); builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { String aName = input.getText().toString(); if(!aName.isEmpty()) { moveFiles(aName); finish(); } else { Toast.makeText(context, getResources().getText(R.string.toast_invalid_album_name), Toast.LENGTH_SHORT).show(); openSaveDialog(); } } }); builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.cancel(); finish(); } }); builder.show(); } /** * Show dialog that informs the user about how to use the app. * Close app when closed. */ private void handleStartFromHomeScreen() { final Context context = this; AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getResources().getText(R.string.dialog_title_on_start_from_home_screen)); builder.setMessage(getResources().getText(R.string.dialog_content_on_start_from_home_screen)); builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.cancel(); finish(); } }); builder.setNegativeButton(R.string.action_settings, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Intent intent = new Intent(context, SettingsActivity.class); startActivity(intent); //handleUnimplementedFeature(); } }); builder.show(); } /** * Move selected files to a new album. The album is located at appDirectoryPath/albumName * @param albumName name of the Album to move the files to */ private void moveFiles(String albumName) { for(String srcDest : sharedMedia) { if(!moveImageFromTo(srcDest, calcTargetPath(albumName, srcDest))) { System.out.println("Fail: " +srcDest); } } } /** * Move file src to target. * @param src url of the file that will be moved * @param target url of the destination * @return success */ private boolean moveImageFromTo(String src, String target) { if(!src.equals(target)) new LoadInitialImageData().doInBackground(src,target); return true; } private void deleteFileFromMediaStore(String path) { if(getMimeType(path).startsWith("image/")) { String[] retCol = {MediaStore.Images.Media._ID}; Cursor cur = this.getContentResolver().query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, retCol, MediaStore.MediaColumns.DATA+"='"+path+"'", null, null); if (cur.getCount() == 0) { Log.e(Tag, "Could not find image "+path+". Therefore can't delete file from MediaStore"); return; } cur.moveToFirst(); int id = cur.getInt(cur.getColumnIndex(MediaStore.MediaColumns._ID)); cur.close(); Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id); this.getContentResolver().delete(uri, null, null); Log.v(Tag, "Deleted image "+path+" from MediaStore as "+uri); } else if(getMimeType(path).startsWith("video/")) { String[] retCol = {MediaStore.Video.Media._ID}; Cursor cur = this.getContentResolver().query( MediaStore.Video.Media.EXTERNAL_CONTENT_URI, retCol, MediaStore.MediaColumns.DATA+"='"+path+"'",null,null); if(cur.getCount() == 0) { Log.e(Tag, "Could not find video "+path+". Therefore can't delete file from MediaStore"); return; } cur.moveToFirst(); int id = cur.getInt(cur.getColumnIndex(MediaStore.MediaColumns._ID)); cur.close(); Uri uri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id); this.getContentResolver().delete(uri, null, null); Log.v(Tag, "Deleted video "+path+" from MediaStore as "+uri); } } /** * Add the file on path to the MediaStore * @param path url of the file */ private void addFileToMediaStore(String path) { MediaScannerConnection.scanFile( getApplicationContext(), new String[]{path}, null, new MediaScannerConnection.OnScanCompletedListener() { @Override public void onScanCompleted(String path, Uri uri) { Log.v(Tag, "File "+path+" was scanned successfully: "+uri); } }); } /** * calculate the new filepath for a file according to the album name * @param albumName name of the Album * @param srcDest url of the file * @return full url of the destination */ private String calcTargetPath(String albumName, String srcDest) { String file = getAppDirectoryPath()+albumName+srcDest.substring(srcDest.lastIndexOf("/")); Log.v(Tag, "Path for "+srcDest+" in "+albumName+" will be "+file); return file; } /** * Set the apps directory path to /Pictures/Albums * @return new path */ public String resetAppDirectoryPath() { String path = Environment.getExternalStorageDirectory().getAbsolutePath()+"/Pictures/Albums/"; Log.d(Tag, "AppDirectoryPath = "+path); SharedPreferences.Editor prefs = PreferenceManager.getDefaultSharedPreferences(this).edit(); prefs.putString(SettingsActivity.KEY_PREF_APP_PATH, path); prefs.apply(); return path; } /** * return the current app directory path * @return path where to store new albums */ public String getAppDirectoryPath() { SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this); String appDirectoryPath = sharedPref.getString(SettingsActivity.KEY_PREF_APP_PATH, ""); if(!appDirectoryPath.isEmpty()) return appDirectoryPath; else { return resetAppDirectoryPath(); } } /** * Return a list of Strings that represent the names of the folders on dirPath * @param dirPath path of the directory * @return list of subfolders of the directory */ public ArrayList getFoldersOnDirectory(String dirPath) { File parent = new File(dirPath); File[] cont = parent.listFiles(); ArrayList subDirs = new ArrayList<>(); if(cont != null) { for (File f : cont) { if (f.isDirectory()) subDirs.add(f.getName()); } } return subDirs; } /** * Get a real path from a uri. * @param context context * @param contentUri uri * @return real path */ public String getRealPathFromURI(Context context, Uri contentUri) { String[] projection = {MediaStore.Images.Media.DATA}; Cursor cursor = context.getContentResolver().query(contentUri, projection, null, null, null); if(cursor == null) return null; int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); cursor.moveToFirst(); String realPath = cursor.getString(column_index); cursor.close(); Log.v(Tag, "Real path for file "+contentUri+" is "+realPath); return realPath; } /** * Return the mimeType of the file on url * @param url url * @return mimeType (eg. "image/jpg") */ public static String getMimeType(String url) { String type = null; String extension = MimeTypeMap.getFileExtensionFromUrl(url); if (extension != null) { MimeTypeMap mime = MimeTypeMap.getSingleton(); type = mime.getMimeTypeFromExtension(extension); } return type; } /** * String[] = {src, target} */ private class LoadInitialImageData extends AsyncTask { @Override protected Void doInBackground(String... params) { InputStream in; OutputStream out; String src = params[0]; String target = params[1]; try { File dir = new File(src.substring(0, src.lastIndexOf("/"))); if (!dir.exists()) { handleUnsupportedMedia(); return null; } in = new FileInputStream(src); File outFile = new File(target.substring(0, target.lastIndexOf("/"))); if (!outFile.exists()) if(!outFile.mkdirs()) { handleCouldNotWrite(outFile.getAbsolutePath()); return null; } out = new FileOutputStream(target); byte[] buffer = new byte[1024]; int read; while ((read = in.read(buffer)) != -1) { out.write(buffer, 0, read); } in.close(); out.flush(); out.close(); addFileToMediaStore(target); deleteFileFromMediaStore(src); } catch (FileNotFoundException e) { e.printStackTrace(); return null; } catch (IOException e) { e.printStackTrace(); return null; } return null; } @Override protected void onPostExecute(Void v) { } } }