├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── scopes │ └── scope_settings.xml └── vcs.xml ├── .project ├── README.md ├── app ├── .gitignore ├── app.iml ├── build.gradle ├── libs │ └── Parse-1.8.1.jar ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml.sample │ ├── java │ └── com │ │ └── cloudinary │ │ └── photoalbum │ │ ├── Constants.java │ │ ├── DownloadImageTask.java │ │ ├── L.java │ │ ├── ListPhotosActivity.java │ │ ├── LoginActivity.java │ │ ├── PhotoAlbumApplication.java │ │ ├── ShowPhotoActivity.java │ │ ├── SplashScreenActivity.java │ │ └── UploadPhotoActivity.java │ └── res │ ├── drawable-xxhdpi │ ├── cloudinary_icon.png │ └── cloudinary_title.png │ ├── layout │ ├── activity_list_photos.xml │ ├── activity_login.xml │ ├── activity_show_photo.xml │ ├── activity_splash_screen.xml │ ├── activity_upload_photo.xml │ ├── fragment_show_photo.xml │ ├── item_grid_image.xml │ └── signin.xml │ ├── menu │ ├── list_photos.xml │ └── upload_photo.xml │ ├── values-large │ └── styles.xml │ ├── values-sw600dp │ ├── dimens.xml │ └── values-sw720dp-land │ │ └── dimens.xml │ ├── values-sw720dp-land │ └── dimens.xml │ ├── values-v14 │ └── styles.xml │ └── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ ├── strings_activity_login.xml │ └── styles.xml ├── build.gradle ├── cloudinary_android_parse_sample.iml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── third_party_licenses.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | /build 7 | AndroidManifest.xml 8 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Cloudinary Android Parse Sample 2 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/scopes/scope_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | PhotoAlbum 4 | 5 | 6 | CloudinaryAndroid 7 | 8 | 9 | 10 | com.android.ide.eclipse.adt.ResourceManagerBuilder 11 | 12 | 13 | 14 | 15 | com.android.ide.eclipse.adt.PreCompilerBuilder 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javabuilder 21 | 22 | 23 | 24 | 25 | com.android.ide.eclipse.adt.ApkBuilder 26 | 27 | 28 | 29 | 30 | 31 | com.android.ide.eclipse.adt.AndroidNature 32 | org.eclipse.jdt.core.javanature 33 | 34 | 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | :warning: Following [Parse's end of service notification](http://blog.parse.com/announcements/moving-on/), this repo is deprecated and is not supported. The issues section won't be available. 2 | 3 | 4 | Cloudinary, Android and Parse - Photo Album sample 5 | ================================================== 6 | 7 | Demo for writing an Android application using Parse as a backend and Cloudinary as image backend for storing, 8 | applying transformations and serving of images. 9 | 10 | For more details about **Cloudinary**: http://cloudinary.com/ 11 | 12 | For more details about **Android development**: http://developer.android.com/develop/index.html 13 | 14 | For more details about **Parse**: https://www.parse.com/ 15 | 16 | 17 | ## Setup and run the sample project 18 | 19 | * [Setup the Parse backend in 7 simple steps](https://github.com/cloudinary/cloudinary_parse#setup-the-sample-project) 20 | * Setup a working [Android Studio](http://developer.android.com/sdk/index.html). 21 | * Clone or [download](https://github.com/cloudinary/cloudinary-android-parse-sample/archive/master.zip) this repository 22 | * Copy the `AndroidManifest.xml.sample` file into `AndroidManifest.xml` and modify the meta-data fields within it to reflect your Cloudinary cloud name and your Parse Application ID and Client Key. 23 | * Import your project (from Android Studio welcome screen > "Open an existing Android Studio project" -> [Choose the cloudinary-android-parse-sample path]) 24 | * Run the application on an Android device or a simulator (Run -> Run 'App' -> [Choose device to run on]) 25 | 26 | ## How does it work 27 | The application is composed of 5 android activities (`SplashScreenActivity`, `LoginActivity`, `ListPhotoActivity`, `ShowPhotoActivity` and `UploadPhotoActivity`), the application singleton (`PhotoAlbumApplication`), a few helper classes and a few external libraries. 28 | 29 | ### Upload process 30 | The Cloudinary API key and API secret are protected in Parse cloud code. They are not bundled in the application and are not accessible by any user - making the design more secure and protecting yours and your users' privacy and data integrity. 31 | 32 | In order to upload in image from the application: 33 | 34 | * A request to upload is sent by the application to Parse backend using a cloud function. 35 | * The cloud function (by default, `sign_upload_request`) creates a signed request (using Cloudinary API parameters) and returns it to the application. 36 | * The application uploads the image to Cloudinary using the retrieved signed request. When the upload is complete, Cloudinary returns a signed result of the operation containing the image public\_id, other identifiers required to access the image and metadata about the image (disk size, dimensions, etc...) 37 | * The application saves the signed reference of the image to Parse backend. 38 | * The Parse beforeSave filter verifies the authenticity of the saved reference and aborts the process if it's invalid. 39 | 40 | ### PhotoAlbumApplication.java 41 | Contains initializations of Universal Image Loader (UIL), Parse and Cloudinary. Cloudinary is initialized using the `CLOUDINARY_URL` meta-data entry in AndroidManifest.xml by passing the `Context` to the Cloudinary constructor 42 | 43 | ### Activities 44 | #### SplashScreenActivity.java 45 | Displays a splash-screen and then launches the LoginActivity 46 | 47 | #### LoginActivity.java 48 | Displays a login screen. The Chuck Norris image demonstrates the usage of Cloudinary Facebook profile image retrieval (See [DownloadImageTask.java](#downloadimagetaskjava) for more information about the image download code) 49 | After the user has provided a username and a password, invokes a task which tries to register (if username doesn't exist yet) or login on behalf of the user using Parse. 50 | 51 | #### ListPhotoActivity.java 52 | Displays an endless list of uploaded images. The list is fetched from Parse by querying the `PARSE_MODEL`. The images themselves are retrieved from Cloudinary. 53 | When an image is clicked, the `ShowPhotoActivity` is launched passing it the image identifier in the intent extra parameters. 54 | This activity also has a menu with upload, refresh and logout actions. 55 | 56 | Based partially on [ImageGridActivity.java](https://github.com/nostra13/Android-Universal-Image-Loader/blob/v1.9.2/sample/src/com/nostra13/example/universalimageloader/ImageGridActivity.java) from Universal Image Loader 57 | 58 | #### ShowPhotoActivity.java 59 | Displays a slidable pager with different transformations of the selected photo. 60 | 61 | Based on [Android Blank Activity with Scrollable Tabs + Swipe template](http://developer.android.com/tools/projects/templates.html#blank-activity) 62 | 63 | #### UploadPhotoActivity.java 64 | Sends an image selection intent to allow the user to select an image to upload. 65 | Once the user selects an image, the [upload process](#uploadprocess) begins. 66 | 67 | ### Helper classes 68 | #### Constants.java 69 | Contains several configuration parameters. 70 | 71 | #### DownloadImageTask.java 72 | A simple but useful AsyncTask which fetches an image by given URL into a given ImageView in the background 73 | 74 | #### L.java 75 | Logging helper class 76 | 77 | ### Libraries 78 | #### cloudinary-android:1.1.2 (*gradle) 79 | The Cloudinary library used to upload and retrieve Cloudinary images 80 | 81 | #### Parse-1.8.1.jar 82 | The Parse library used for user management (sign-up, login), and image objects management (add, list, fetch) 83 | 84 | #### universalimageloader:universal-image-loader:1.9.3 (*gradle) 85 | A very comprehensive and flexible library for ["asynchronous image loading, caching and displaying"](https://github.com/nostra13/Android-Universal-Image-Loader#-universal-image-loader-for-android) 86 | 87 | ## Read more 88 | 89 | [Cloudinary documentation](http://cloudinary.com/documentation) 90 | [Cloudinary for Android](https://github.com/cloudinary/cloudinary_android) 91 | [Cloudinary for Parse](https://github.com/cloudinary/cloudinary_parse) 92 | 93 | # Support 94 | 95 | You can [open an issue through GitHub](https://github.com/cloudinary/cloudinary-android-parse-sample/issues). 96 | 97 | Contact us at [info@cloudinary.com](mailto:info@cloudinary.com) 98 | 99 | Or via Twitter: [@cloudinary](https://twitter.com/#!/cloudinary) 100 | 101 | ## Licenses 102 | 103 | * This project is released under the MIT license. 104 | * [Parse third-party licenses](https://github.com/cloudinary/cloudinary-android-parse-sample/blob/master/third_party_licenses.txt) 105 | * [Android-Universal-Image-Loader](https://github.com/nostra13/Android-Universal-Image-Loader) is distributed under [the Apache License Version 2.0](https://github.com/nostra13/Android-Universal-Image-Loader/blob/master/LICENSE) 106 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 21 5 | buildToolsVersion "21.1.2" 6 | 7 | defaultConfig { 8 | applicationId "com.cloudinary.photoalbum.PhotoAlbumApplication" 9 | minSdkVersion 17 10 | targetSdkVersion 21 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile 'com.android.support:support-v4:21.0.3' 24 | compile 'com.cloudinary:cloudinary-android:1.1.2' 25 | compile 'com.parse.bolts:bolts-android:1.1.4' 26 | compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.3' 27 | compile files('libs/Parse-1.8.1.jar') 28 | compile fileTree(dir: 'libs', include: ['*.jar']) 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/libs/Parse-1.8.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudinary/cloudinary_android_parse_sample/8f0e62bf92186e3451e62de9f61388e0a36186fb/app/libs/Parse-1.8.1.jar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml.sample: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 42 | 45 | 46 | 49 | 50 | 54 | 57 | 58 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/cloudinary/photoalbum/Constants.java: -------------------------------------------------------------------------------- 1 | package com.cloudinary.photoalbum; 2 | 3 | public final class Constants { 4 | private Constants() { 5 | } 6 | public static final String TAG = "CloudinarySample"; 7 | public static final String EXTRA_PHOTO = "com.cloudinary.photo"; 8 | public static final String PARSE_MODEL = "Photo"; 9 | public static final String PARSE_CLOUDINARY_FIELD = "cloudinaryIdentifier"; 10 | public static final String PARSE_SIGN_CLOUD_FUNCTION = "sign_cloudinary_upload_request"; 11 | public static final int SPLASH_SCREEN_TIMEOUT = 1000; 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/cloudinary/photoalbum/DownloadImageTask.java: -------------------------------------------------------------------------------- 1 | package com.cloudinary.photoalbum; 2 | 3 | import java.io.InputStream; 4 | 5 | import android.graphics.Bitmap; 6 | import android.graphics.BitmapFactory; 7 | import android.os.AsyncTask; 8 | import android.widget.ImageView; 9 | 10 | /** 11 | * A simple task that download an image from a given url into an Image View 12 | * Based on http://stackoverflow.com/a/9288544/967435 13 | */ 14 | public class DownloadImageTask extends AsyncTask { 15 | ImageView bmImage; 16 | 17 | public DownloadImageTask(ImageView bmImage) { 18 | this.bmImage = bmImage; 19 | } 20 | 21 | protected Bitmap doInBackground(String... urls) { 22 | String urldisplay = urls[0]; 23 | Bitmap bitmap = null; 24 | try { 25 | InputStream in = new java.net.URL(urldisplay).openStream(); 26 | bitmap = BitmapFactory.decodeStream(in); 27 | } catch (Exception e) { 28 | L.e(e, "Error fetching image"); 29 | } 30 | return bitmap; 31 | } 32 | 33 | protected void onPostExecute(Bitmap result) { 34 | bmImage.setImageBitmap(result); 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cloudinary/photoalbum/L.java: -------------------------------------------------------------------------------- 1 | package com.cloudinary.photoalbum; 2 | 3 | import android.util.Log; 4 | 5 | /** 6 | * "Less-word" analog of Android {@link Log logger} 7 | * 8 | * Based on Android-Universal-Image-Loader (https://github.com/nostra13/Android-Universal-Image-Loader) 9 | */ 10 | public final class L { 11 | 12 | private static final String LOG_FORMAT = "%1$s\n%2$s"; 13 | private static String logTag = "uninitialized"; 14 | 15 | private L() { 16 | } 17 | 18 | public static void setTag(String tag) { 19 | logTag = tag; 20 | } 21 | 22 | public static void d(String message, Object... args) { 23 | log(Log.DEBUG, null, message, args); 24 | } 25 | 26 | public static void i(String message, Object... args) { 27 | log(Log.INFO, null, message, args); 28 | } 29 | 30 | public static void w(String message, Object... args) { 31 | log(Log.WARN, null, message, args); 32 | } 33 | 34 | public static void e(Throwable ex) { 35 | log(Log.ERROR, ex, null); 36 | } 37 | 38 | public static void e(String message, Object... args) { 39 | log(Log.ERROR, null, message, args); 40 | } 41 | 42 | public static void e(Throwable ex, String message, Object... args) { 43 | log(Log.ERROR, ex, message, args); 44 | } 45 | 46 | private static void log(int priority, Throwable ex, String message, Object... args) { 47 | if (args.length > 0) { 48 | message = String.format(message, args); 49 | } 50 | 51 | String log; 52 | if (ex == null) { 53 | log = message; 54 | } else { 55 | String logMessage = message == null ? ex.getMessage() : message; 56 | String logBody = Log.getStackTraceString(ex); 57 | log = String.format(LOG_FORMAT, logMessage, logBody); 58 | } 59 | Log.println(priority, logTag, log); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/cloudinary/photoalbum/ListPhotosActivity.java: -------------------------------------------------------------------------------- 1 | package com.cloudinary.photoalbum; 2 | 3 | import java.util.List; 4 | 5 | import android.app.Activity; 6 | import android.app.AlertDialog; 7 | import android.content.DialogInterface; 8 | import android.content.Intent; 9 | import android.os.Bundle; 10 | import android.view.Menu; 11 | import android.view.MenuItem; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | import android.widget.AdapterView; 15 | import android.widget.AdapterView.OnItemClickListener; 16 | import android.widget.BaseAdapter; 17 | import android.widget.GridView; 18 | import android.widget.ImageView; 19 | 20 | import com.cloudinary.Cloudinary; 21 | import com.cloudinary.Transformation; 22 | import com.nostra13.universalimageloader.core.ImageLoader; 23 | import com.parse.ParseException; 24 | import com.parse.ParseObject; 25 | import com.parse.ParseQuery; 26 | import com.parse.ParseQuery.CachePolicy; 27 | 28 | public class ListPhotosActivity extends Activity { 29 | static final int REQUEST_UPLOAD = 1; 30 | 31 | protected Cloudinary cloudinary; 32 | protected ImageLoader imageLoader = ImageLoader.getInstance(); 33 | protected ParseImageAdapter adapter; 34 | protected GridView listView; 35 | 36 | @Override 37 | protected void onCreate(Bundle savedInstanceState) { 38 | super.onCreate(savedInstanceState); 39 | 40 | // Cloudinary: Retrieve and save initialized Cloudinary instance 41 | cloudinary = PhotoAlbumApplication.getInstance(this).getCloudinary(); 42 | 43 | setContentView(R.layout.activity_list_photos); 44 | listView = (GridView) findViewById(R.id.gridView1); 45 | adapter = new ParseImageAdapter(); 46 | listView.setAdapter(adapter); 47 | listView.setOnItemClickListener(new OnItemClickListener() { 48 | @Override 49 | public void onItemClick(AdapterView parent, View view, int position, long id) { 50 | try { 51 | showImage(adapter.getIdentifier(position)); 52 | } catch (ParseException e) { 53 | L.e(e, "Error getting identifier"); 54 | errorMessage("Error getting identifier for image to show transformations: " + e.toString()); 55 | } 56 | 57 | } 58 | }); 59 | } 60 | 61 | private void errorMessage(String errorMessage) { 62 | new AlertDialog.Builder(this) 63 | .setTitle("Error") 64 | .setMessage(errorMessage) 65 | .setPositiveButton("OK",new DialogInterface.OnClickListener() { 66 | public void onClick(DialogInterface dialog, int whichButton) { 67 | finish(); 68 | } 69 | }) 70 | .setCancelable(true) 71 | .create().show(); 72 | } 73 | 74 | private void showImage(String identifier) { 75 | Intent intent = new Intent(this, ShowPhotoActivity.class); 76 | intent.putExtra(Constants.EXTRA_PHOTO, identifier); 77 | startActivity(intent); 78 | } 79 | 80 | @Override 81 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 82 | if (requestCode == REQUEST_UPLOAD && resultCode == RESULT_OK) { 83 | adapter.clearCache(); 84 | } 85 | } 86 | 87 | @Override 88 | public boolean onCreateOptionsMenu(Menu menu) { 89 | // Inflate the menu; this adds items to the action bar if it is present. 90 | getMenuInflater().inflate(R.menu.list_photos, menu); 91 | return true; 92 | } 93 | 94 | @Override 95 | public boolean onOptionsItemSelected(MenuItem item) { 96 | Intent intent; 97 | switch (item.getItemId()) { 98 | case R.id.action_upload: 99 | intent = new Intent(this, UploadPhotoActivity.class); 100 | startActivityForResult(intent, REQUEST_UPLOAD); 101 | break; 102 | case R.id.action_refresh: 103 | adapter.clearCache(); 104 | break; 105 | case R.id.action_logout: 106 | intent = new Intent(this, LoginActivity.class); 107 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); 108 | startActivity(intent); 109 | break; 110 | } 111 | return false; 112 | } 113 | 114 | public class ParseImageAdapter extends BaseAdapter { 115 | private static final int ITEM_PER_FETCH = 20; 116 | private ParseQuery query; 117 | private int cachePosition; 118 | private List cache = null; 119 | private Transformation thumbnailTransformation = new Transformation().width(120).height(120).crop("fill"); 120 | 121 | public ParseImageAdapter() { 122 | createQuery(); 123 | } 124 | 125 | private void createQuery() { 126 | // Parse: Create a query for model Photo and set caching options 127 | query = ParseQuery.getQuery(Constants.PARSE_MODEL); 128 | query.setCachePolicy(CachePolicy.CACHE_ELSE_NETWORK); 129 | } 130 | 131 | private String getIdentifier(int index) throws ParseException { 132 | index = getCount() - 1 - index; 133 | int base = index-(index%ITEM_PER_FETCH); 134 | if (cache == null || cachePosition != base) { 135 | L.d("Fetching %d items since %d", ITEM_PER_FETCH, base); 136 | // Parse: Fetch ITEM_PER_FETCH items since index "base" 137 | query.setSkip(base); 138 | query.setLimit(ITEM_PER_FETCH); 139 | cache = query.find(); 140 | cachePosition = base; 141 | } 142 | // Parse: Get identifier field from parse object 143 | ParseObject photo = cache.get(index % ITEM_PER_FETCH); 144 | String identifier = photo.getString(Constants.PARSE_CLOUDINARY_FIELD); 145 | L.d("Returning identifier: %s for index %d", identifier, index); 146 | return identifier; 147 | } 148 | 149 | private String getUrl(int index) throws ParseException { 150 | String identifier = getIdentifier(index); 151 | // Cloudinary: generate a URL reflecting the thumbnail transformation on the given identifier. 152 | String url = cloudinary.url().fromIdentifier(identifier).transformation(thumbnailTransformation).generate(); 153 | return url; 154 | } 155 | 156 | private void clearCache() { 157 | L.d("Clearing cache. Cache policy: %s", query.getCachePolicy().toString()); 158 | cache = null; 159 | ParseQuery.clearAllCachedResults(); 160 | createQuery(); 161 | query.setCachePolicy(CachePolicy.NETWORK_ONLY); 162 | notifyDataSetChanged(); 163 | } 164 | 165 | @Override 166 | public int getCount() { 167 | try { 168 | int count; 169 | count = query.count(); 170 | return count; 171 | } catch (ParseException e) { 172 | e.printStackTrace(); 173 | throw new RuntimeException("Can't query object count"); 174 | } 175 | } 176 | 177 | @Override 178 | public Object getItem(int position) { 179 | return null; 180 | } 181 | 182 | @Override 183 | public long getItemId(int position) { 184 | return position; 185 | } 186 | 187 | @Override 188 | public View getView(int position, View convertView, ViewGroup parent) { 189 | final ImageView imageView; 190 | if (convertView == null) { 191 | imageView = (ImageView) getLayoutInflater().inflate(R.layout.item_grid_image, parent, false); 192 | } else { 193 | imageView = (ImageView) convertView; 194 | } 195 | 196 | try { 197 | imageLoader.displayImage(getUrl(position), imageView); 198 | } catch (ParseException e) { 199 | L.e(e, "Error getting identifier"); 200 | errorMessage("Error getting identifier for image to show in list: " + e.toString()); 201 | } 202 | 203 | return imageView; 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /app/src/main/java/com/cloudinary/photoalbum/LoginActivity.java: -------------------------------------------------------------------------------- 1 | package com.cloudinary.photoalbum; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.annotation.TargetApi; 6 | import android.app.Activity; 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.os.AsyncTask; 10 | import android.os.Build; 11 | import android.os.Bundle; 12 | import android.support.v4.app.NavUtils; 13 | import android.text.TextUtils; 14 | import android.view.KeyEvent; 15 | import android.view.MenuItem; 16 | import android.view.View; 17 | import android.view.inputmethod.EditorInfo; 18 | import android.widget.EditText; 19 | import android.widget.ImageView; 20 | import android.widget.TextView; 21 | 22 | import com.cloudinary.Cloudinary; 23 | import com.cloudinary.Transformation; 24 | import com.parse.ParseException; 25 | import com.parse.ParseQuery; 26 | import com.parse.ParseUser; 27 | 28 | /** 29 | * Activity which displays a login screen to the user, offering registration as 30 | * well. 31 | */ 32 | public class LoginActivity extends Activity { 33 | /** 34 | * Keep track of the login task to ensure we can cancel it if requested. 35 | */ 36 | private UserLoginTask mAuthTask = null; 37 | 38 | private Context mContext = this; 39 | 40 | // Values for user and password at the time of the login attempt. 41 | private String mUser; 42 | private String mPassword; 43 | 44 | // UI references. 45 | private EditText mUserView; 46 | private EditText mPasswordView; 47 | private View mLoginFormView; 48 | private View mLoginStatusView; 49 | private TextView mLoginStatusMessageView; 50 | 51 | @Override 52 | protected void onCreate(Bundle savedInstanceState) { 53 | super.onCreate(savedInstanceState); 54 | 55 | setContentView(R.layout.activity_login); 56 | setupActionBar(); 57 | 58 | setupImage(); 59 | 60 | // Set up the login form. 61 | mUserView = (EditText) findViewById(R.id.user); 62 | mUserView.setText(mUser); 63 | 64 | mPasswordView = (EditText) findViewById(R.id.password); 65 | mPasswordView 66 | .setOnEditorActionListener(new TextView.OnEditorActionListener() { 67 | @Override 68 | public boolean onEditorAction(TextView textView, int id, 69 | KeyEvent keyEvent) { 70 | if (id == R.id.login || id == EditorInfo.IME_NULL) { 71 | attemptLogin(); 72 | return true; 73 | } 74 | return false; 75 | } 76 | }); 77 | 78 | mLoginFormView = findViewById(R.id.login_form); 79 | mLoginStatusView = findViewById(R.id.login_status); 80 | mLoginStatusMessageView = (TextView) findViewById(R.id.login_status_message); 81 | 82 | findViewById(R.id.sign_in_button).setOnClickListener( 83 | new View.OnClickListener() { 84 | @Override 85 | public void onClick(View view) { 86 | attemptLogin(); 87 | } 88 | }); 89 | } 90 | 91 | void setupImage() { 92 | Cloudinary cloudinary = PhotoAlbumApplication.getInstance(this).getCloudinary(); 93 | String url = cloudinary.url().type("facebook").transformation( 94 | new Transformation().height(95).width(95).crop("thumb").gravity("face").effect("sepia").radius(20) 95 | .chain().angle(10) 96 | ).format("png").generate("officialchucknorrispage"); 97 | new DownloadImageTask((ImageView) findViewById(R.id.logo)) 98 | .execute(url); 99 | } 100 | 101 | /** 102 | * Set up the {@link android.app.ActionBar}, if the API is available. 103 | */ 104 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 105 | private void setupActionBar() { 106 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 107 | // Show the Up button in the action bar. 108 | getActionBar().setDisplayHomeAsUpEnabled(true); 109 | } 110 | } 111 | 112 | @Override 113 | public boolean onOptionsItemSelected(MenuItem item) { 114 | switch (item.getItemId()) { 115 | case android.R.id.home: 116 | // This ID represents the Home or Up button. In the case of this 117 | // activity, the Up button is shown. Use NavUtils to allow users 118 | // to navigate up one level in the application structure. For 119 | // more details, see the Navigation pattern on Android Design: 120 | // 121 | // http://developer.android.com/design/patterns/navigation.html#up-vs-back 122 | // 123 | NavUtils.navigateUpFromSameTask(this); 124 | return true; 125 | } 126 | return super.onOptionsItemSelected(item); 127 | } 128 | 129 | /** 130 | * Attempts to sign in or register the account specified by the login form. 131 | * If there are form errors (invalid user, missing fields, etc.), the 132 | * errors are presented and no actual login attempt is made. 133 | */ 134 | public void attemptLogin() { 135 | if (mAuthTask != null) { 136 | return; 137 | } 138 | 139 | // Reset errors. 140 | mUserView.setError(null); 141 | mPasswordView.setError(null); 142 | 143 | // Store values at the time of the login attempt. 144 | mUser = mUserView.getText().toString(); 145 | mPassword = mPasswordView.getText().toString(); 146 | 147 | boolean cancel = false; 148 | View focusView = null; 149 | 150 | // Check for a valid password. 151 | if (TextUtils.isEmpty(mPassword)) { 152 | mPasswordView.setError(getString(R.string.error_field_required)); 153 | focusView = mPasswordView; 154 | cancel = true; 155 | } else if (mPassword.length() < 4) { 156 | mPasswordView.setError(getString(R.string.error_invalid_password)); 157 | focusView = mPasswordView; 158 | cancel = true; 159 | } 160 | 161 | // Check for a valid user 162 | if (TextUtils.isEmpty(mUser)) { 163 | mUserView.setError(getString(R.string.error_field_required)); 164 | focusView = mUserView; 165 | cancel = true; 166 | } 167 | 168 | if (cancel) { 169 | // There was an error; don't attempt login and focus the first 170 | // form field with an error. 171 | focusView.requestFocus(); 172 | } else { 173 | // Show a progress spinner, and kick off a background task to 174 | // perform the user login attempt. 175 | mLoginStatusMessageView.setText(R.string.login_progress_signing_in); 176 | showProgress(true); 177 | mAuthTask = new UserLoginTask(); 178 | mAuthTask.execute((Void) null); 179 | } 180 | } 181 | 182 | /** 183 | * Shows the progress UI and hides the login form. 184 | */ 185 | @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2) 186 | private void showProgress(final boolean show) { 187 | // On Honeycomb MR2 we have the ViewPropertyAnimator APIs, which allow 188 | // for very easy animations. If available, use these APIs to fade-in 189 | // the progress spinner. 190 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) { 191 | int shortAnimTime = getResources().getInteger( 192 | android.R.integer.config_shortAnimTime); 193 | 194 | mLoginStatusView.setVisibility(View.VISIBLE); 195 | mLoginStatusView.animate().setDuration(shortAnimTime) 196 | .alpha(show ? 1 : 0) 197 | .setListener(new AnimatorListenerAdapter() { 198 | @Override 199 | public void onAnimationEnd(Animator animation) { 200 | mLoginStatusView.setVisibility(show ? View.VISIBLE 201 | : View.GONE); 202 | } 203 | }); 204 | 205 | mLoginFormView.setVisibility(View.VISIBLE); 206 | mLoginFormView.animate().setDuration(shortAnimTime) 207 | .alpha(show ? 0 : 1) 208 | .setListener(new AnimatorListenerAdapter() { 209 | @Override 210 | public void onAnimationEnd(Animator animation) { 211 | mLoginFormView.setVisibility(show ? View.GONE 212 | : View.VISIBLE); 213 | } 214 | }); 215 | } else { 216 | // The ViewPropertyAnimator APIs are not available, so simply show 217 | // and hide the relevant UI components. 218 | mLoginStatusView.setVisibility(show ? View.VISIBLE : View.GONE); 219 | mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); 220 | } 221 | } 222 | 223 | /** 224 | * Represents an asynchronous login/registration task used to authenticate 225 | * the user. 226 | */ 227 | public class UserLoginTask extends AsyncTask { 228 | @Override 229 | protected Boolean doInBackground(Void... params) { 230 | ParseUser user; 231 | try { 232 | // Parse: Look up given user 233 | L.d("UserLoginTask::doInBackground - checking if user '%s' exists", mUser); 234 | ParseQuery query = ParseQuery.getQuery(ParseUser.class).whereEqualTo("username", mUser); 235 | if (query.count() == 0) { 236 | L.i("UserLoginTask::doInBackground - doesn't exist - signing up"); 237 | // Parse: User doesn't exist - create 238 | user = new ParseUser(); 239 | user.setUsername(mUser); 240 | user.setPassword(mPassword); 241 | user.signUp(); 242 | } else { 243 | L.d("UserLoginTask::doInBackground - exists - verifying password"); 244 | // Parse: User exists - verify password 245 | ParseUser.logIn(mUser, mPassword); 246 | } 247 | L.d("UserLoginTask::doInBackground - done!"); 248 | 249 | return true; 250 | } catch (ParseException e) { 251 | L.e(e, "Authentication error"); 252 | return false; 253 | } 254 | } 255 | 256 | @Override 257 | protected void onPostExecute(final Boolean success) { 258 | mAuthTask = null; 259 | showProgress(false); 260 | 261 | if (success) { 262 | Intent intent = new Intent(mContext, ListPhotosActivity.class); 263 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); 264 | startActivity(intent); 265 | } else { 266 | mPasswordView 267 | .setError(getString(R.string.error_incorrect_password)); 268 | mPasswordView.requestFocus(); 269 | } 270 | } 271 | 272 | @Override 273 | protected void onCancelled() { 274 | mAuthTask = null; 275 | showProgress(false); 276 | } 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /app/src/main/java/com/cloudinary/photoalbum/PhotoAlbumApplication.java: -------------------------------------------------------------------------------- 1 | package com.cloudinary.photoalbum; 2 | 3 | import static com.cloudinary.photoalbum.Constants.TAG; 4 | import android.app.Application; 5 | import android.content.Context; 6 | import android.content.pm.PackageManager; 7 | import android.content.pm.PackageManager.NameNotFoundException; 8 | import android.os.Bundle; 9 | 10 | import com.cloudinary.Cloudinary; 11 | import com.cloudinary.android.Utils; 12 | import com.nostra13.universalimageloader.core.DisplayImageOptions; 13 | import com.nostra13.universalimageloader.core.ImageLoader; 14 | import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; 15 | import com.parse.Parse; 16 | import com.parse.ParseACL; 17 | 18 | public class PhotoAlbumApplication extends Application { 19 | private Cloudinary cloudinary; 20 | 21 | /** 22 | * @return An initialized Cloudinary instance 23 | */ 24 | public Cloudinary getCloudinary() { 25 | return cloudinary; 26 | } 27 | 28 | /** 29 | * Provides access to the singleton and the getCloudinary method 30 | * @param context Android Application context 31 | * @return instance of the Application singleton. 32 | */ 33 | public static PhotoAlbumApplication getInstance(Context context) { 34 | return (PhotoAlbumApplication)context.getApplicationContext(); 35 | } 36 | 37 | /** 38 | * Initializes UIL, Parse and Cloudinary upon creation of Application. 39 | */ 40 | @Override 41 | public void onCreate() { 42 | L.setTag(TAG); 43 | initUIL(); 44 | initParse(); 45 | initCloudinary(); 46 | } 47 | 48 | private void initUIL() { 49 | DisplayImageOptions defaultOptions = new DisplayImageOptions.Builder() 50 | .cacheInMemory(true) 51 | .cacheOnDisk(true) 52 | .build(); 53 | ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(getApplicationContext()) 54 | .defaultDisplayImageOptions(defaultOptions) 55 | .build(); 56 | ImageLoader.getInstance().init(config); 57 | L.i("Universal Image Loader initialized"); 58 | } 59 | 60 | private void initParse() { 61 | String appId = null; 62 | String clientKey = null; 63 | 64 | try { 65 | Bundle bundle = getPackageManager() 66 | .getApplicationInfo( getPackageName(), PackageManager.GET_META_DATA) 67 | .metaData; 68 | appId = bundle.getString("PARSE_APPLICATION_ID"); 69 | clientKey = bundle.getString("PARSE_CLIENT_KEY"); 70 | } catch (NameNotFoundException e) { 71 | // fall-thru 72 | } catch (NullPointerException e) { 73 | // fall-thru 74 | } 75 | if (appId == null || clientKey == null) { 76 | throw new RuntimeException("Couldn't load Parse meta-data params from manifest"); 77 | } 78 | 79 | 80 | /// Parse - Initializes with appId and clientKey from manifest 81 | Parse.initialize(this, appId, clientKey); 82 | 83 | ParseACL defaultACL = new ParseACL(); 84 | ParseACL.setDefaultACL(defaultACL, true); 85 | L.i("Parse initialized"); 86 | } 87 | 88 | private void initCloudinary() { 89 | // Cloudinary: creating a cloudinary instance using meta-data from manifest 90 | 91 | cloudinary = new Cloudinary(Utils.cloudinaryUrlFromContext(this)); 92 | L.i("Cloudinary initialized"); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/com/cloudinary/photoalbum/ShowPhotoActivity.java: -------------------------------------------------------------------------------- 1 | package com.cloudinary.photoalbum; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Locale; 5 | 6 | import android.content.Context; 7 | import android.os.Bundle; 8 | import android.support.v4.app.Fragment; 9 | import android.support.v4.app.FragmentActivity; 10 | import android.support.v4.app.FragmentManager; 11 | import android.support.v4.app.FragmentPagerAdapter; 12 | import android.support.v4.view.ViewPager; 13 | import android.util.Pair; 14 | import android.view.LayoutInflater; 15 | import android.view.View; 16 | import android.view.ViewGroup; 17 | import android.widget.ImageView; 18 | 19 | import com.cloudinary.Cloudinary; 20 | import com.cloudinary.Transformation; 21 | import com.nostra13.universalimageloader.core.ImageLoader; 22 | 23 | public class ShowPhotoActivity extends FragmentActivity { 24 | /** 25 | * The {@link android.support.v4.view.PagerAdapter} that will provide 26 | * fragments for each of the sections. We use a 27 | * {@link android.support.v4.app.FragmentPagerAdapter} derivative, which 28 | * will keep every loaded fragment in memory. If this becomes too memory 29 | * intensive, it may be best to switch to a 30 | * {@link android.support.v4.app.FragmentStatePagerAdapter}. 31 | */ 32 | SectionsPagerAdapter mSectionsPagerAdapter; 33 | 34 | /** 35 | * The {@link ViewPager} that will host the section contents. 36 | */ 37 | ViewPager mViewPager; 38 | 39 | Context context; 40 | String cloudinaryIdentifier; 41 | 42 | /** 43 | * Cloudinary: List of transformations and names to display 44 | */ 45 | @SuppressWarnings("serial") 46 | static ArrayList> transformations = new ArrayList>() {{ 47 | add(new Pair("Original", new Transformation())); 48 | add(new Pair("Round fill", new Transformation().width(400).height(700).crop("fill").radius(10))); 49 | add(new Pair("Scale", new Transformation().width(400).height(700).crop("scale"))); 50 | add(new Pair("Fit", new Transformation().width(400).height(700).crop("fit"))); 51 | add(new Pair("Thumb + face", new Transformation().width(400).height(700).crop("thumb").gravity("face"))); 52 | add(new Pair("Shabang", new Transformation().width(400).height(700).crop("fill").gravity("north").chain().angle(20).chain().effect("sepia"))); 53 | }}; 54 | 55 | @Override 56 | protected void onCreate(Bundle savedInstanceState) { 57 | super.onCreate(savedInstanceState); 58 | setContentView(R.layout.activity_show_photo); 59 | 60 | // Create the adapter that will return a fragment for each of the three 61 | // primary sections of the app. 62 | mSectionsPagerAdapter = new SectionsPagerAdapter( 63 | getSupportFragmentManager()); 64 | 65 | // Set up the ViewPager with the sections adapter. 66 | mViewPager = (ViewPager) findViewById(R.id.pager); 67 | mViewPager.setAdapter(mSectionsPagerAdapter); 68 | 69 | Bundle bundle = getIntent().getExtras(); 70 | cloudinaryIdentifier = bundle.getString(Constants.EXTRA_PHOTO); 71 | 72 | context = this; 73 | } 74 | 75 | /** 76 | * A {@link FragmentPagerAdapter} that returns a fragment corresponding to 77 | * one of the sections/tabs/pages. 78 | */ 79 | public class SectionsPagerAdapter extends FragmentPagerAdapter { 80 | 81 | public SectionsPagerAdapter(FragmentManager fm) { 82 | super(fm); 83 | } 84 | 85 | @Override 86 | public Fragment getItem(int position) { 87 | // getItem is called to instantiate the fragment for the given page. 88 | // Return a DummySectionFragment (defined as a static inner class 89 | // below) with the page number as its lone argument. 90 | Fragment fragment = new ImageByURLSectionFragment(); 91 | Bundle args = new Bundle(); 92 | 93 | Cloudinary cloudinary = PhotoAlbumApplication.getInstance(context).getCloudinary(); 94 | Pair transformation = transformations.get(position); 95 | // Cloudinary: generate a URL reflecting the given transformation on the given identifier. 96 | String url = cloudinary.url().fromIdentifier(cloudinaryIdentifier).transformation(transformation.second).generate(); 97 | args.putString(ImageByURLSectionFragment.ARG_URL, url); 98 | fragment.setArguments(args); 99 | return fragment; 100 | } 101 | 102 | @Override 103 | public int getCount() { 104 | return transformations.size(); 105 | } 106 | 107 | @Override 108 | public CharSequence getPageTitle(int position) { 109 | Pair transformation = transformations.get(position); 110 | Locale l = Locale.getDefault(); 111 | return transformation.first.toUpperCase(l); 112 | } 113 | } 114 | 115 | /** 116 | * A fragment representing a section of the app 117 | * displays an image 118 | */ 119 | public static class ImageByURLSectionFragment extends Fragment { 120 | /** 121 | * The fragment argument representing the section number for this 122 | * fragment. 123 | */ 124 | public static final String ARG_URL = "url"; 125 | 126 | public ImageByURLSectionFragment() { 127 | } 128 | 129 | @Override 130 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 131 | Bundle savedInstanceState) { 132 | View rootView = inflater.inflate( 133 | R.layout.fragment_show_photo, container, false); 134 | 135 | ImageView imageView = (ImageView) rootView.findViewById(R.id.image); 136 | ImageLoader.getInstance().displayImage(getArguments().getString(ARG_URL), imageView); 137 | 138 | return rootView; 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cloudinary/photoalbum/SplashScreenActivity.java: -------------------------------------------------------------------------------- 1 | package com.cloudinary.photoalbum; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.os.Handler; 7 | 8 | /** 9 | * An example full-screen activity that shows and hides the system UI (i.e. 10 | * status bar and navigation/system bar) with user interaction. 11 | * 12 | * @see SystemUiHider 13 | */ 14 | public class SplashScreenActivity extends Activity { 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | L.d("SplashScreen - created"); 19 | 20 | setContentView(R.layout.activity_splash_screen); 21 | scheduleRedirect(); 22 | } 23 | 24 | private void scheduleRedirect() { 25 | // Calls login activity after splash screen timeout 26 | final Activity current = this; 27 | new Handler().postDelayed(new Runnable() { 28 | public void run() { 29 | Intent intent = new Intent(current, LoginActivity.class); 30 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); 31 | startActivity(intent); 32 | } 33 | }, Constants.SPLASH_SCREEN_TIMEOUT); 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cloudinary/photoalbum/UploadPhotoActivity.java: -------------------------------------------------------------------------------- 1 | package com.cloudinary.photoalbum; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | import android.annotation.TargetApi; 9 | import android.app.Activity; 10 | import android.app.AlertDialog; 11 | import android.app.ProgressDialog; 12 | import android.content.DialogInterface; 13 | import android.content.Intent; 14 | import android.database.Cursor; 15 | import android.net.Uri; 16 | import android.os.AsyncTask; 17 | import android.os.Build; 18 | import android.os.Bundle; 19 | import android.provider.MediaStore; 20 | import android.support.v4.app.NavUtils; 21 | import android.view.Menu; 22 | import android.view.MenuItem; 23 | 24 | import com.cloudinary.Cloudinary; 25 | import com.parse.ParseCloud; 26 | import com.parse.ParseException; 27 | import com.parse.ParseObject; 28 | 29 | public class UploadPhotoActivity extends Activity { 30 | private final static int SELECT_PICTURE = 1; 31 | private final Activity current = this; 32 | private ProgressDialog dialog = null; 33 | 34 | @Override 35 | protected void onCreate(Bundle savedInstanceState) { 36 | super.onCreate(savedInstanceState); 37 | 38 | Intent intent = new Intent(); 39 | intent.setType("image/*"); 40 | intent.setAction(Intent.ACTION_GET_CONTENT); 41 | startActivityForResult(Intent.createChooser(intent, "Select Picture"), SELECT_PICTURE); 42 | } 43 | 44 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 45 | if (requestCode == SELECT_PICTURE) { 46 | if (resultCode == RESULT_OK) { 47 | setDefaultLayout(); 48 | Uri selectedImageUri = data.getData(); 49 | L.d("Uploading file from URI: %s", selectedImageUri.getPath()); 50 | String[] filePathColumn = {MediaStore.Images.Media.DATA}; 51 | 52 | Cursor cursor = getContentResolver().query( 53 | selectedImageUri, filePathColumn, null, null, null); 54 | cursor.moveToFirst(); 55 | 56 | int columnIndex = cursor.getColumnIndex(filePathColumn[0]); 57 | String filePath = cursor.getString(columnIndex); 58 | cursor.close(); 59 | L.d("Uploading file: %s", filePath); 60 | startUpload(filePath); 61 | } 62 | } 63 | } 64 | 65 | private void setDefaultLayout() { 66 | setContentView(R.layout.activity_upload_photo); 67 | // Show the Up button in the action bar. 68 | setupActionBar(); 69 | } 70 | 71 | private void startUpload(String filePath) { 72 | AsyncTask task = new AsyncTask() { 73 | protected String doInBackground(String... paths) { 74 | L.d("Running upload task"); 75 | 76 | // sign request 77 | Map uploadParams; 78 | try { 79 | // Parse+Cloudinary: retrieves a Cloudinary signature and upload params using the Parse cloud function. 80 | // see https://github.com/cloudinary/cloudinary_parse 81 | HashMap args = new HashMap(); 82 | uploadParams = ParseCloud.callFunction(Constants.PARSE_SIGN_CLOUD_FUNCTION ,args); 83 | L.i("Signed request: %s", uploadParams.toString()); 84 | } catch (ParseException e) { 85 | L.e(e, "Error signing request"); 86 | return "Error signing request: " + e.toString(); 87 | } 88 | 89 | // Upload to cloudinary 90 | Cloudinary cloudinary = PhotoAlbumApplication.getInstance(current).getCloudinary(); 91 | File file = new File(paths[0]); 92 | @SuppressWarnings("rawtypes") 93 | Map cloudinaryResult; 94 | try { 95 | // Cloudinary: Upload file using the retrieved signature and upload params 96 | cloudinaryResult = cloudinary.uploader().upload(file, uploadParams); 97 | L.i("Uploaded file: %s", cloudinaryResult.toString()); 98 | } catch (RuntimeException e) { 99 | L.e(e, "Error uploading file"); 100 | return "Error uploading file: " + e.toString(); 101 | } catch (IOException e) { 102 | L.e(e, "Error uploading file"); 103 | return "Error uploading file: " + e.toString(); 104 | } 105 | 106 | // update parse 107 | ParseObject photo = new ParseObject("Photo"); 108 | try { 109 | // Parse+Cloudinary: Save a reference to the uploaded image in Parse backend. The 110 | // field may be verified using the beforeSave filter demonstrated in: 111 | // https://github.com/cloudinary/cloudinary_parse 112 | photo.put(Constants.PARSE_CLOUDINARY_FIELD, cloudinary.signedPreloadedImage(cloudinaryResult)); 113 | photo.save(); 114 | L.i("Saved object"); 115 | } catch (Exception e) { 116 | L.e(e, "Error saving object"); 117 | return "Error saving object: " + e.toString(); 118 | } 119 | return null; 120 | } 121 | 122 | protected void onPostExecute(String error) { 123 | if (dialog != null) { 124 | dialog.dismiss(); 125 | dialog = null; 126 | } 127 | if (error == null) { 128 | setResult(RESULT_OK); 129 | finish(); 130 | } else { 131 | new AlertDialog.Builder(current) 132 | .setTitle("Error") 133 | .setMessage(error) 134 | .setPositiveButton("OK",new DialogInterface.OnClickListener() { 135 | public void onClick(DialogInterface dialog, int whichButton) { 136 | finish(); 137 | } 138 | }) 139 | .setCancelable(true) 140 | .create().show(); 141 | } 142 | } 143 | }; 144 | dialog = ProgressDialog.show(this, "Uploading", "Uploading image"); 145 | task.execute(filePath); 146 | } 147 | 148 | /** 149 | * Set up the {@link android.app.ActionBar}, if the API is available. 150 | */ 151 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 152 | private void setupActionBar() { 153 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 154 | getActionBar().setDisplayHomeAsUpEnabled(true); 155 | } 156 | } 157 | 158 | @Override 159 | public boolean onCreateOptionsMenu(Menu menu) { 160 | // Inflate the menu; this adds items to the action bar if it is present. 161 | getMenuInflater().inflate(R.menu.upload_photo, menu); 162 | return true; 163 | } 164 | 165 | @Override 166 | public boolean onOptionsItemSelected(MenuItem item) { 167 | switch (item.getItemId()) { 168 | case android.R.id.home: 169 | // This ID represents the Home or Up button. In the case of this 170 | // activity, the Up button is shown. Use NavUtils to allow users 171 | // to navigate up one level in the application structure. For 172 | // more details, see the Navigation pattern on Android Design: 173 | // 174 | // http://developer.android.com/design/patterns/navigation.html#up-vs-back 175 | // 176 | NavUtils.navigateUpFromSameTask(this); 177 | return true; 178 | } 179 | return super.onOptionsItemSelected(item); 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/cloudinary_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudinary/cloudinary_android_parse_sample/8f0e62bf92186e3451e62de9f61388e0a36186fb/app/src/main/res/drawable-xxhdpi/cloudinary_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/cloudinary_title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudinary/cloudinary_android_parse_sample/8f0e62bf92186e3451e62de9f61388e0a36186fb/app/src/main/res/drawable-xxhdpi/cloudinary_title.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_list_photos.xml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 15 | 16 | 21 | 22 | 30 | 31 | 32 | 33 | 34 | 38 | 39 | 42 | 43 | 51 | 52 | 63 | 64 |