package dan.dit.whatsthat.util.webPhotoSharing; import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Implementing requests for dumpyourphoto.com API as specified by * https://github.com/DumpYourPhoto/API-Documentation. * Service unfortunately is scheduled to shut down on january the first in 2016. :( * * Created by daniel on 27.10.15. */ public class DumpYourPhotoController extends PhotoAlbumShareController { /** * The accepted format of the response, specified in the header. Do not change. API supports * html, json, csv, xml. * The output for the url field is buggy for csv since this would be a map (an array) of urls. * */ private static final String ACCEPT_HEADER = "application/csv"; // though this should not be public, I trust people to not mess with my account and this key private static final String API_KEY_PARAMETER = "api_key=kfxCwdBY5OpxfxGerTKFM9MEOQYwxSFYdHSeCHEQ1zxXKaTopnImFDETk2pTdRSjP6umuAAWuXVPX4GFVtaaw99ukbFbxIvt6iVY"; private static final String BASE_DOWNLOAD_URL = "https://static.dyp.im/"; private static final String BASE_URL = "https://api.dumpyourphoto.com/v1/"; /** * If the album that gets created for each user once will be public or not. */ public static final boolean IS_ALBUM_PUBLIC_DEFAULT = true; private static @Nullable HttpURLConnection makeOpenConnection(@Nullable String urlString) { if (TextUtils.isEmpty(urlString)) { return null; } URL url; try { url = new URL(urlString); } catch (MalformedURLException e) { Log.e("DumpYourPhoto", "Error creating url for dump your photo: " + e); return null; } HttpURLConnection urlConnection; try { urlConnection = (HttpURLConnection) url.openConnection(); } catch (IOException ioe) { Log.e("DumpYourPhoto", "Error opening connection: " + ioe); return null; } return urlConnection; } private static ResponseMap obtainResponse(String requestMethod, String urlString) { HttpURLConnection urlConnection = makeOpenConnection(urlString); if (urlConnection == null) { return null; } ResponseMap response = null; try { urlConnection.setRequestMethod(requestMethod); urlConnection.setRequestProperty("Accept", ACCEPT_HEADER); InputStream in = new BufferedInputStream(urlConnection.getInputStream()); response = new ResponseMap(in); } catch (IOException ioe) { Log.e("DumpYourPhoto", "Error with opened url connection for obtaining response: " + ioe); } finally { urlConnection.disconnect(); } return response; } private static ResponseMap postOrPutDataObtainResponse(boolean post, HttpURLConnection urlConnection) { if (urlConnection == null) { return null; } ResponseMap response = null; try { urlConnection.setRequestMethod(post ? "POST" : "PUT"); urlConnection.setRequestProperty("Accept", ACCEPT_HEADER); InputStream in = new BufferedInputStream(urlConnection.getInputStream()); response = new ResponseMap(in); } catch (IOException ioe) { Log.e("DumpYourPhoto", "Error with opened url connection for posting/putting data: " + ioe); } finally { urlConnection.disconnect(); } return response; } private static ResponseMap postBitmapDataObtainResponse(String urlString, File bitmapFile) { HttpURLConnection urlConnection = makeOpenConnection(urlString); if (urlConnection == null || bitmapFile == null || !bitmapFile.exists()) { return null; // no sense in trying } ResponseMap response = null; try { urlConnection.setUseCaches(false); urlConnection.setDoOutput(true); urlConnection.setRequestMethod("POST"); urlConnection.setRequestProperty("Accept", ACCEPT_HEADER); urlConnection.setRequestProperty("Connection", "Keep-Alive"); // required? urlConnection.setRequestProperty("Cache-Control", "no-cache"); final String crlf = "\r\n"; final String twoHypens ="--"; final String boundary = "WebKitFormBoundary7MA4YWxkTrZu0gW"; urlConnection.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary); // start content wrapper urlConnection.connect(); OutputStream requestbase = urlConnection.getOutputStream(); OutputStream request = new BufferedOutputStream(requestbase); request.write((twoHypens + boundary + crlf).getBytes()); request.write(("Content-Disposition:form-data; name=\"files\"" + "; filename=\"" + bitmapFile.getName() + "\"" + crlf + "Content-Type: image/png" + crlf + crlf).getBytes ()); // For sending bitmap as file FileInputStream fileStream = new FileInputStream(bitmapFile); byte[] buffer = new byte[1024]; int read; while ((read = fileStream.read(buffer)) != -1) { request.write(buffer, 0, read); } // end content wrapper request.write((crlf + twoHypens + boundary + twoHypens + crlf).getBytes()); request.flush(); request.close(); final int responseCode = urlConnection.getResponseCode(); if (responseCode != HttpURLConnection.HTTP_CREATED && responseCode != HttpURLConnection .HTTP_OK) { Log.e("DumpYourPhoto", "Response when uploading not ok or created: " + urlConnection .getResponseCode() + " for url " + urlString + " response=" + urlConnection.getResponseMessage()); return null; } // get response InputStream in = new BufferedInputStream(urlConnection.getInputStream()); response = new ResponseMap(in); } catch (IOException e) { Log.e("DumpYourPhoto", "Error posting bitmap data : " + e); } finally { urlConnection.disconnect(); } return response; } private static ResponseMap deleteAlbum(String albumHash) { if (TextUtils.isEmpty(albumHash)) { return null; } return obtainResponse("DELETE", BASE_URL + "albums/" + albumHash); } private static ResponseMap getAlbums() { return obtainResponse("GET", BASE_URL + "albums" + "?" + API_KEY_PARAMETER); } /** * Creates a new album on the website dumpyourphoto.com. Returns the album's hash * to identify it. If creation fails for some reasons, null is returned. * @param albumName The non empty album name to use to create the new album. Should identify * the user. * @return Null if creation fails for some reason or the hash of the newly created empty album. */ public String makeAlbum(String albumName) { ResponseMap map = makeAlbumExecute(albumName); if (map == null) { return null; } return map.getEntry("hash", 0, null); } /** * Updates the album with the given hash to the new name and new public state. * @param albumHash The album to update. The hash that got returned when creating the album. * Must be non empty to actually update the album. * @param newAlbumName The new album's name. Must be non empty to actually update the album. * @param newIsPublic If the album should become public or private. * @return The album's hash if everything went fine, else null. */ public String updateAlbum(String albumHash, String newAlbumName, boolean newIsPublic) { ResponseMap map = updateAlbumExecute(albumHash, newAlbumName, newIsPublic); if (map == null) { return null; } return map.getEntry("hash", 0, null); } private static ResponseMap updateAlbumExecute(String albumHash, String newAlbumName, boolean newIsPublic) { if (TextUtils.isEmpty(albumHash)) { return null; } if (newAlbumName == null) { return null; } StringBuilder url = new StringBuilder(); url.append(BASE_URL) .append("albums/") .append(albumHash) .append("?") .append(API_KEY_PARAMETER); try { String encoded = URLEncoder.encode(newAlbumName, "UTF-8"); url.append('&') .append("name=") .append(encoded); } catch (UnsupportedEncodingException e) { Log.e("DumpYourPhoto", "Error encoding url: " + url + " : " + e); return null; } url.append('&') .append("public=") .append(newIsPublic ? '1' : '0'); HttpURLConnection urlConnection = makeOpenConnection(url.toString()); if (urlConnection == null) { return null; } urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); // Returns album with keys: id, name, hash, public, photos. ResponseMap response = postOrPutDataObtainResponse(false, urlConnection); Log.d("DumpYourPhoto", "Album created response: " + response); return response; } private static ResponseMap makeAlbumExecute(String albumName) { if (albumName == null) { return null; } StringBuilder url = new StringBuilder(); url.append(BASE_URL) .append("albums") .append("?") .append(API_KEY_PARAMETER); try { String encoded = URLEncoder.encode(albumName, "UTF-8"); url.append('&') .append("name=") .append(encoded); } catch (UnsupportedEncodingException e) { Log.e("DumpYourPhoto", "Error encoding url: " + url + " : " + e); return null; } url.append('&') .append("public=") .append(IS_ALBUM_PUBLIC_DEFAULT ? '1' : '0'); HttpURLConnection urlConnection = makeOpenConnection(url.toString()); if (urlConnection == null) { return null; } urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); // Returns album with keys: id, name, hash, public, photos. ResponseMap response = postOrPutDataObtainResponse(true, urlConnection); Log.d("DumpYourPhoto", "Album created response: " + response); return response; } /** * Uploadds the given bitmap file to the given album. * @param albumHash The album's hash. Album needs to be already created, must be non empty. * @param bitmapFile The valid file to an image. Can be any valid supported image format. * @return The hash of the uploaded photo inside the given album together with the photo's * server filename. This information is required to downloading the photo from the server. * Format: photohash/photofilename. Can be null if something goes wrong. Photohash or * photofilename can be empty if some very unexpected server error happens. */ public @Nullable String uploadPhotoToAlbum(String albumHash, File bitmapFile) { ResponseMap map = uploadPhotoToAlbumExecute(albumHash, bitmapFile); if (map == null) { return null; } return map.getEntry("hash", 0, "") + "/" + map.getEntry("file_name", 0, ""); } private static ResponseMap uploadPhotoToAlbumExecute(String albumHash, File bitmapFile) { if (TextUtils.isEmpty(albumHash) || bitmapFile == null || !bitmapFile.exists()) { Log.e("DumpYourPhoto", "No need to try uploading photo " + bitmapFile + " to album "+ albumHash); return null; // no need to try } Log.d("DumpYourPhoto", "Uploading to album file name " + bitmapFile.getName()); ResponseMap response = postBitmapDataObtainResponse(BASE_URL + "albums/" + albumHash + "/photos?" + API_KEY_PARAMETER, bitmapFile); Log.d("DumpYourPhoto", "Uploaded photo to album, response: " + response); return response; } /** * Recreates the download link from the given photoUploadLink that was returned when the * photo was successfully uploaded to the server. * @param photoUploadLink The upload link of the photo, returned by uploadPhotoToAlbum(). * @return The download link to use to retrieve the uploaded image. */ private static @NonNull String makeDownloadLinkExecute(@NonNull String photoUploadLink) { return BASE_DOWNLOAD_URL + (photoUploadLink.startsWith("/") ? photoUploadLink .substring(1) : photoUploadLink); } public @Nullable URL makeShareLink(@NonNull String photoLink) { try { return new URL(makeDownloadLinkExecute(photoLink)); } catch (MalformedURLException e) { Log.e("HomeStuff", "Error creating share link from " + photoLink + " : " + e); return null; } } public @Nullable URL makeDownloadLink(@NonNull Uri shared) { List<String> segments = shared.getPathSegments(); if (segments != null && segments.size() > 1) { try { return new URL(DumpYourPhotoController.makeDownloadLinkExecute(segments.get(segments.size() - 2) + "/" + segments.get(segments.size() - 1))); } catch (MalformedURLException e) { Log.e("HomeStuff", "Illegal url for making download link of shared: " + shared + " to " + e); return null; } } return null; } /** * Helper class for parsing and storing the server's response to a request on a http connection. */ private static class ResponseMap { private Map<String, String[]> mData; /** * Creates a new ResponseMap reading the given response stream and closing it afterwards. * @param response The response to read from the server. * @throws IOException If some error happens while reading the response. */ public ResponseMap(InputStream response) throws IOException { InputStreamReader reader = new InputStreamReader(response); StringBuilder totalResult = new StringBuilder(); int read; char[] buffer = new char[1024]; try { while ((read = reader.read(buffer)) > 0) { totalResult.append(buffer, 0, read); } } catch (IOException ioe) { Log.e("DumpYourPhoto", "Error during creating response map: " + ioe); } String[] keysAndValues = totalResult.toString().split("\n"); if (keysAndValues.length < 2) { throw new IOException("Illegal response, too little data to form response map: " + totalResult.toString()); } String[] keys = keysAndValues[0].split(","); mData = new HashMap<>(keys.length); for (int valuesIndex = 1; valuesIndex < keysAndValues.length; valuesIndex++) { String[] values = keysAndValues[valuesIndex].split(","); for (int i = 0; i < Math.min(keys.length, values.length); i++) { String currKey = cropQuotationMarks(keys[i]); String[] currValues = mData.get(currKey); if (currValues == null) { currValues = new String[keysAndValues.length - 1]; mData.put(currKey, currValues); } currValues[valuesIndex - 1] = (cropQuotationMarks(values[i])); } } response.close(); } private static String cropQuotationMarks(String toCrop) { if (toCrop.length() == 0) { return toCrop; } final char QUOTATION_MARK = '\"'; if (toCrop.length() == 1) { return toCrop.charAt(0) == QUOTATION_MARK ? "" : toCrop; } // string has at least two characters if (toCrop.charAt(0) == QUOTATION_MARK) { toCrop = toCrop.substring(1); } if (toCrop.charAt(toCrop.length() - 1) == QUOTATION_MARK) { toCrop = toCrop.substring(0, toCrop.length() - 1); } return toCrop; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append('{'); boolean addKeySeparator = false; for (String key : mData.keySet()) { if (addKeySeparator) { builder.append(", "); } addKeySeparator = true; builder.append(key) .append("=["); boolean addComma = false; for (String value : mData.get(key)) { if (addComma) { builder.append(", "); } addComma = true; builder.append(value); } builder.append("]"); } builder.append('}'); return builder.toString(); } /** * Returns the entry for the given key at the given index. Index starts with 0 for the * first entry. * @param key The entry's key. * @param index The entry's index. * @param defaultValue The default value if the index is out of bounds or no data is * available for the given key. * @return The requested mapped data or the given defaultValue if not mapped or nothing * mapped for given index. */ public String getEntry(String key, int index, String defaultValue) { String[] data = mData.get(key); if (data == null || index < 0 || index >= data.length) { return defaultValue; } return data[index]; } } }