{"id":21698,"date":"2018-04-10T17:21:37","date_gmt":"2018-04-10T17:21:37","guid":{"rendered":"http:\/\/building_image_storyboard_apps_on_android_a_tutorial"},"modified":"2024-06-03T13:20:32","modified_gmt":"2024-06-03T20:20:32","slug":"building_image_storyboard_apps_on_android_a_tutorial","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial","title":{"rendered":"Building Image Storyboard Apps on Android: A Tutorial"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><p>Globally, approximately two billion people now own smartphones, which also feature cameras capable of capturing photos and videos of a tonal richness and quality unimaginable even five years ago. Until recently, those cameras behaved mostly as optical sensors, catching light that determines  the resulting image\u2019s pixels. The next generation of cameras, however, can  blend hardware and computer-vision algorithms that apply to an image\u2019s semantic content, spawning creative mobile photo and video apps.<\/p>\n<p>In early February, Google launched three mobile apps in their <code>appsperiment<\/code> scheme, which leverage such technologies as object recognition, person segmentation, image encoding and decoding, and garnering of  user response. This tutorial focuses on one of those apps, Storyboard on Android, in which you transform videos into single-page comic layouts, all on devices. After you have selected a video and loaded it in Storyboard, the app picks interesting video frames, compresses JPEGs, and displays them on screen. You\u2019ll learn how to replicate these features on Android with <a href=\"https:\/\/cloudinary.com\/\">Cloudinary<\/a>, a cloud-based, end-to-end solution for storing, optimizing, and delivering images and videos. Impressively, with Cloudinary, you can transform images and videos, that is, resize and add numerous effects to them.<\/p>\n<p>So, to recap, , this tutorial walks you through the process of  uploading a video to Cloudinary, in which you then generate, and deliver image thumbnails of any frame from the uploaded file, and render it on screen without having to compress the video. Furthermore, you will add effects to the generated image thumbnails and take them live through unique URLs.<\/p>\n<p>As reference, see the <a href=\"https:\/\/cloudinary.com\/blog\/introducing_the_complete_video_solution_for_web_and_mobile_developers\">Cloudinary post on working with videos<\/a> on the computer or on mobile.<\/p>\n<p>For details on image optimization and its background concepts, read this <a href=\"https:\/\/cloudinary.com\/blog\/image_optimization_expert_roundup\">Cloudinary post<\/a>.<\/p>\n<h2>Demo and Source Code<\/h2>\n<p>This <a href=\"https:\/\/drive.google.com\/file\/d\/1q-iR411aFwy5TjLhtFHELtwAfwjbzjml\/view?usp=sharing\">short clip<\/a> shows you how Storyboard works. To run it on an Android device, <a href=\"https:\/\/drive.google.com\/drive\/my-drive?zx=qxrpk6k5xvg5\">download the app to your Google Drive<\/a>.<\/p>\n<p>While building the app, you might want to check out the <a href=\"https:\/\/github.com\/Kennypee\/ImageBoard-CLoudinary\">source code<\/a>.<\/p>\n<h2>Requirements<\/h2>\n<p>As a prerequisite, set up a <a href=\"https:\/\/cloudinary.com\/users\/register\/free\">Cloudinary account<\/a>.<\/p>\n<p>Also,download Android Studio (or an IDE of your choice) and two third-party libraries:<\/p>\n<ul>\n<li>\n<a href=\"https:\/\/developer.android.com\/studio\/\">Android Studio<\/a>\n<\/li>\n<li>\n<a href=\"https:\/\/cloudinary.com\/\">Cloudinary<\/a> Android SDK<\/li>\n<li>\n<a href=\"http:\/\/square.github.io\/picasso\/\">Picasso<\/a> for asynchronous image loading<\/li>\n<\/ul>\n<h2>Setup of Android<\/h2>\n<p>As a first step, open Android Studio and create an Android Studio project:<\/p>\n<p>. Choose File &gt; New &gt; New Project.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-res.cloudinary.com\/image\/upload\/w_600,c_fill\/dpr_auto\/s_one.jpg\" alt=\"New Project\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1200\" height=\"1012\"\/><\/p>\n<pre><code>In the **Create Android Project** dialog box, fill in the **Application name**, **Project location**, and **Package name** fields. Click **Next**.\n\n![name the app and click next](https:\/\/cloudinary-res.cloudinary.com\/image\/upload\/w_600,c_fill\/dpr_auto\/Named_App.jpg)\n<\/code><\/pre>\n<p>Under <strong>Select the form factors and minimum SDK<\/strong> in the <strong>Target Android Devices<\/strong> dialog box, select <strong>API 17<\/strong>: <strong>Android 4.2 (Jelly Bean)<\/strong>. Click <strong>Next<\/strong>.Select target devices and API levels.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-res.cloudinary.com\/image\/upload\/w_600,c_fill\/dpr_auto\/SDK_to_API_17.jpg\" alt=\"set minimum sdk to API 17 and click next\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1200\" height=\"1009\"\/><\/p>\n<p>In the <strong>Add an Activity to Mobile<\/strong> dialog box, select Add No Activity. Click <strong>Next<\/strong>.<\/p>\n<pre><code>![select empty activity and click next](https:\/\/cloudinary-res.cloudinary.com\/image\/upload\/w_600,c_fill\/dpr_auto\/Android_Activity.jpg)\n<\/code><\/pre>\n<p>In the <strong>Configure Activity<\/strong> dialog box, leave the default settings as is. Click <strong>Finish<\/strong>.<\/p>\n<pre><code>![leave the defaults and click finish](https:\/\/cloudinary-res.cloudinary.com\/image\/upload\/w_600,c_fill\/dpr_auto\/Configure_Activity.jpg)\n<\/code><\/pre>\n<h2>Setup of Dependencies<\/h2>\n<p>Next, add the code below to install Cloudinary and Picasso as a dependency in the <code>build.gradle<\/code> file of your app module:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">    implementation group: <span class=\"hljs-string\">'com.cloudinary'<\/span>, <span class=\"hljs-attr\">name<\/span>: <span class=\"hljs-string\">'cloudinary-android'<\/span>, <span class=\"hljs-attr\">version<\/span>: <span class=\"hljs-string\">'1.22.0'<\/span>\n    implementation <span class=\"hljs-string\">'com.squareup.picasso:picasso:2.5.2'<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Click <strong>Sync<\/strong> to install.<\/p>\n<p>Afterwards, open the <code>AndroidManifest.xml<\/code> file and add Cloudinary configurations under the application tag:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\">    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">manifest<\/span> <span class=\"hljs-attr\">xmlns:android<\/span>=<span class=\"hljs-string\">\"http:\/\/schemas.android.com\/apk\/res\/android\"<\/span>\n       <span class=\"hljs-attr\">package<\/span>=<span class=\"hljs-string\">\"com.example.ekene.imageboardcloudinary\"<\/span>&gt;<\/span>\n    \n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">uses-permission<\/span> <span class=\"hljs-attr\">android:name<\/span>=<span class=\"hljs-string\">\"android.permission.INTERNET\"<\/span>\/&gt;<\/span>\n    \n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">application<\/span>\n            <span class=\"hljs-attr\">...<\/span>\n            &gt;<\/span>\n            <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">meta-data<\/span>\n                <span class=\"hljs-attr\">android:name<\/span>=<span class=\"hljs-string\">\"CLOUDINARY_URL\"<\/span>\n                <span class=\"hljs-attr\">android:value<\/span>=<span class=\"hljs-string\">\"cloudinary:\/\/@myCloudName\"<\/span>\/&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">application<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">manifest<\/span>&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p><code>myCloudName<\/code> is your Cloudinary name, which is on your <a href=\"https:\/\/cloudinary.com\/users\/login\">console<\/a>. Note that the Internet permissions are already in the manifest.<\/p>\n<h2>App Layout<\/h2>\n<p>Now define the app layout with the following user-interface elements:<\/p>\n<ul>\n<li>A bar that shows the progress of the upload process<\/li>\n<li>A button that triggers the video upload<\/li>\n<li>Three <code>ImageView<\/code> objects that render the image thumbnails from Cloudinary<\/li>\n<\/ul>\n<p><a href=\"https:\/\/gist.github.com\/Maharony\/72b42e9acb94213752763cbdadb837db\">activity_main.xml<\/a>\n<em>View Activity main file<\/em><\/p>\n<p>Next, initialize the view objects in the <code>MainActivity.java<\/code> file so you can refer to them later. Edit the file so it reads like this:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\">    package com.example.ekene.imageboardcloudinary;\n    \n    import ....\n    \n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">MainActivity<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">AppCompatActivity<\/span> <span class=\"hljs-keyword\">implements<\/span> <span class=\"hljs-title\">View<\/span>.<span class=\"hljs-title\">OnClickListener<\/span> <\/span>{\n    \n        <span class=\"hljs-keyword\">private<\/span> Button uploadBtn;\n        <span class=\"hljs-keyword\">private<\/span> ProgressBar progressBar;\n        <span class=\"hljs-keyword\">private<\/span> int SELECT_VIDEO = <span class=\"hljs-number\">2<\/span>;\n        <span class=\"hljs-keyword\">private<\/span> ImageView img1, img2, img3;\n    \n        @Override\n        <span class=\"hljs-keyword\">protected<\/span> void onCreate(Bundle savedInstanceState) {\n            super.onCreate(savedInstanceState);\n            setContentView(R.layout.activity_main);\n            img1 = findViewById(R.id.img1);\n            img2 = findViewById(R.id.img2);\n            img3 = findViewById(R.id.img3);\n            progressBar = findViewById(R.id.progress_bar);\n            uploadBtn = findViewById(R.id.uploadBtn);\n        }\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Next, add the code below to set up the <strong>Upload<\/strong> button, which, on a click, launches the gallery in which you can select a video for upload<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">    uploadBtn.setOnClickListener(<span class=\"hljs-keyword\">new<\/span> View.OnClickListener() {\n        @Override\n        public <span class=\"hljs-keyword\">void<\/span> onClick(View view) {\n            pickVideoFromGallery();\n        }\n        private <span class=\"hljs-keyword\">void<\/span> pickVideoFromGallery() {\n            Intent GalleryIntent = <span class=\"hljs-keyword\">new<\/span> Intent();\n            GalleryIntent.setType(<span class=\"hljs-string\">\"video\/*\"<\/span>);\n            GalleryIntent.setAction(Intent.ACTION_GET_CONTENT);\n            startActivityForResult(Intent.createChooser(GlleryIntent, \n            <span class=\"hljs-string\">\"select video\"<\/span>), SELECT_VIDEO);\n        }\n    });\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Subsequently, as soon as you select a video, Cloudinary calls the <code>onActivityResult<\/code> method, which in turn triggers an upload to Cloudinary. To facilitate uploads, create an <code>UploadRequest<\/code> method and dispatch it within the <code>onActivityResult<\/code> method, as described in the next section..<\/p>\n<h2>Cloudinary Uploads<\/h2>\n<p>Cloudinary offers two types of uploads:<\/p>\n<ul>\n<li>\n<p><a href=\"https:\/\/cloudinary.com\/documentation\/android_image_and_video_upload#signed_upload\">Signed<\/a> uploads, which require an authentication signature from a backend. For these uploads, your images and videos are signed with the API and secret key in the console. Because those signatures are risky on the client side, which can be easily decompiled, a back-end is mandatory for security.<\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/cloudinary.com\/documentation\/android_image_and_video_upload#unsigned_upload\">Unsigned<\/a> uploads, which do not require signatures and are thus less secure than signed ones. These uploads are controlled by an upload preset, which defines the options that apply to the images that are uploaded with the preset.<\/p>\n<\/li>\n<\/ul>\n<p>Here, you set up  unsigned uploads by enabling them on the console: Select <strong>Settings<\/strong> on your dashboard, select the <strong>Upload<\/strong> tab, scroll down to <strong>Upload presets<\/strong>, and enable <strong>Unsigned<\/strong>. Cloudinary then generates a preset with a random string as its name. Copy the name and set it aside for use later.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-res.cloudinary.com\/image\/upload\/w_600,c_fill\/dpr_auto\/unsigned_1.jpg\" alt=\"unsigned\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1200\" height=\"617\"\/><\/p>\n<p>Next, enable the upload of a selected video to Cloudinary by first initializing the Cloudinary <code>MediaManager<\/code> class <code>onCreate<\/code> method in the <code>MainActivity<\/code> class, as shown here:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">    package com.example.ekene.imageboardcloudinary;\n    <span class=\"hljs-keyword\">import<\/span> ....\n    \n    public <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">MainActivity<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">AppCompatActivity<\/span> <span class=\"hljs-title\">implements<\/span> <span class=\"hljs-title\">View<\/span>.<span class=\"hljs-title\">OnClickListener<\/span> <\/span>{\n        @Override\n        protected <span class=\"hljs-keyword\">void<\/span> onCreate(Bundle savedInstanceState) {\n            <span class=\"hljs-keyword\">super<\/span>.onCreate(savedInstanceState);\n            setContentView(R.layout.activity_main);\n            <span class=\"hljs-comment\">\/\/initialize MediaManager<\/span>\n            MediaManager.init(<span class=\"hljs-keyword\">this<\/span>);\n        }\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Subsequently, call the <code>onActivityResult()<\/code> method in <code>MainActivity<\/code> and set it up.<\/p>\n<p>As background, here are the methods in <code>MainActivity<\/code> this app has adopted so far and their relationships to <code>onActivityResult<\/code>:<\/p>\n<ul>\n<li>\n<p><code>onCreate<\/code> \u2014 Becomes active when the app starts. The XML file in use earlier is the default layout of the activity. You also added a listener to the button, which, on a click, causes the app to call the second method, <code>pickVideoFromGallery<\/code> (see below).<\/p>\n<\/li>\n<li>\n<p><code>pickVideoFromGallery<\/code> \u2014 Launches the user\u2019s gallery for video selection. This process generates a unique request code in the <code>SELECT_VIDEO<\/code> variable. Because a response follows a video section, the <code>startActivityForResult()<\/code> method renders the response on the <code>onActivityForResult()<\/code> method. If the selection succeeds, so does the response, and the <code>selectedVideo<\/code> variable in turn holds <code>URI<\/code> of the selected video for upload. Finally, Culinary calls the <code>onActivityForResult()<\/code> method (see below). If the selection fails, the process ends.<\/p>\n<\/li>\n<li>\n<p><code>onActivityForResult()<\/code>  \u2014 Checks if the response from <code>startActivityForResult()<\/code> succeeded. On a video selection, <code>resultCode<\/code> is equal to <code>Activity.RESULT_OK<\/code>; otherwise, to <code>Activity.RESULT_CANCELLED<\/code>. A success results in an upload request to Cloudinary with <code>MediaManager<\/code>, like this:<\/p>\n<\/li>\n<\/ul>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\">    @Override\n    <span class=\"hljs-keyword\">protected<\/span> void onActivityResult(int requestCode, int resultCode, <span class=\"hljs-keyword\">final<\/span> Intent data) {\n    \n        <span class=\"hljs-keyword\">if<\/span> (requestCode == SELECT_VIDEO &amp;&amp; resultCode == RESULT_OK) {\n            Uri selectedVideo = data.getData();\n            <span class=\"hljs-comment\">\/\/...<\/span>\n            }\n        }\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Now create an upload request by adding <code>MediaManager<\/code> to <code>onActivityResult<\/code> and build <code>UploadRequest<\/code> in Cloudinary with the following five methods:<\/p>\n<ul>\n<li>\n<code>upload()<\/code> \u2014 Takes in <code>uri<\/code> of the selected video for uploads.<\/li>\n<li>\n<code>unsigned()<\/code> \u2013 Takes in <code>preset name<\/code> from your console.<\/li>\n<li>\n<code>option()<\/code> \u2013 Takes in <code>resource_type<\/code> of the upload.<\/li>\n<\/ul>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">    MediaManager.get()\n                    .upload(selectedVideo)\n                    .unsigned(<span class=\"hljs-string\">\"preset_name\"<\/span>)\n                    .option(<span class=\"hljs-string\">\"resource_type\"<\/span>, <span class=\"hljs-string\">\"video\"<\/span>)\n                    .callback(...)\n                   <span class=\"hljs-comment\">\/\/...<\/span>\n                }\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<ul>\n<li>\n<code>callback()<\/code> \u2014 Takes in a new <code>UploadCallback<\/code> method, which implements several other callback methods that track the progress of the upload.<\/li>\n<li>\n<code>onStart()<\/code> \u2014 Defines what happens when upload starts:<\/li>\n<\/ul>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">    .callback(<span class=\"hljs-keyword\">new<\/span> UploadCallback() {\n                        @Override\n                        public <span class=\"hljs-keyword\">void<\/span> onStart(<span class=\"hljs-built_in\">String<\/span> requestId) {\n                            progressBar.setVisibility(View.VISIBLE);\n                            Toast.makeText(MainActivity.this, \n                            <span class=\"hljs-string\">\"Upload has started...\"<\/span>, Toast.LENGTH_SHORT).show();\n                        }\n                        <span class=\"hljs-comment\">\/\/...<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>The above code makes <code>progressBar<\/code> visible and sets <code>Toast<\/code>, which displays the message \u201cUpload has started.\u201d<\/p>\n<ul>\n<li>\n<code>onProgress()<\/code> \u2014  Defines what happens as the upload progresses, that is, has started but not yet completed. Leave it blank in this app::<\/li>\n<\/ul>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">    .callback(<span class=\"hljs-keyword\">new<\/span> UploadCallback() {\n                       @Override\n                        public <span class=\"hljs-keyword\">void<\/span> onProgress(...) {\n                        }\n                        <span class=\"hljs-comment\">\/\/...<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<ul>\n<li>\n<code>onSuccess()<\/code> \u2014 Defines what happens if the video upload succeeds. In this app, you generate the required images from the uploaded video and pass them to the <code>ImageView<\/code> object with Piccasso:<\/li>\n<\/ul>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">    .callback(<span class=\"hljs-keyword\">new<\/span> UploadCallback() {\n                        @Override\n                        public <span class=\"hljs-keyword\">void<\/span> onSuccess(<span class=\"hljs-built_in\">String<\/span> requestId, <span class=\"hljs-built_in\">Map<\/span> resultData) {\n                            Toast.makeText(MainActivity.this, \n                            <span class=\"hljs-string\">\"Uploaded Succesfully\"<\/span>, Toast.LENGTH_SHORT).show();\n                            progressBar.setVisibility(View.GONE);\n                            uploadBtn.setVisibility(View.INVISIBLE);\n                            <span class=\"hljs-comment\">\/\/ on successful upload, we start generating the images<\/span>\n                            <span class=\"hljs-comment\">\/\/ first get the video unique id<\/span>\n                            <span class=\"hljs-built_in\">String<\/span> publicId = resultData.get(<span class=\"hljs-string\">\"public_id\"<\/span>).toString();\n                            <span class=\"hljs-comment\">\/\/generate the first image from the id <\/span>\n                            <span class=\"hljs-built_in\">String<\/span> firstImgUrl = MediaManager.get().url().transformation(                        <span class=\"hljs-keyword\">new<\/span> Transformation().startOffset(<span class=\"hljs-string\">\"12\"<\/span>).border(\n                            <span class=\"hljs-string\">\"5px_solid_black\"<\/span>).border(<span class=\"hljs-string\">\"5px_solid_black\"<\/span>)).resourceType(\n                            <span class=\"hljs-string\">\"video\"<\/span>).generate(publicId+<span class=\"hljs-string\">\".jpg\"<\/span>);                        \n                            <span class=\"hljs-comment\">\/\/ load the first image into the image view<\/span>\n                            Picasso.with(getApplicationContext()).load(\n                            firstImgUrl).into(img1);\n                        }\n                        <span class=\"hljs-comment\">\/\/...<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>The above <code>onSuccess()<\/code> method outputs <code>requestId<\/code> and details from the upload. You then access the uploaded video\u2019s URL by calling <code>resultData.get(&quot;url&quot;)<\/code> and the video\u2019s ID by calling <code>resultData.get(&quot;public_id&quot;)<\/code>. Subsequently, generate the images from the video ID and store their URLs in a string variable.<\/p>\n<p>Afterwards, load the images into their image view objects with Picasso.<\/p>\n<ul>\n<li>\n<code>onError()<\/code> &#8211; Defines what happens if an error occurs during the upload process. For this app, create a <code>Toast<\/code> method for the error message, like this:<\/li>\n<\/ul>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">    .callback(<span class=\"hljs-keyword\">new<\/span> UploadCallback() {\n                        @Override\n                        public <span class=\"hljs-keyword\">void<\/span> onError(<span class=\"hljs-built_in\">String<\/span> requestId, ErrorInfo error) {\n                            Toast.makeText(MainActivity.this, \n                            <span class=\"hljs-string\">\"Upload Error\"<\/span>, Toast.LENGTH_SHORT).show();\n                            Log.v(<span class=\"hljs-string\">\"ERROR!!\"<\/span>, error.getDescription());\n                        }\n                        <span class=\"hljs-comment\">\/\/...<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<ul>\n<li>\n<code>dispatch()<\/code> \u2014 Defines when and how each upload request runs.<\/li>\n<\/ul>\n<p>You have now set up the <code>onActivityForResult()<\/code> method. When it receives a response from <code>pickVideoFromGallery()<\/code>, it creates a request to upload to Cloudinary with the URL of the selected video. Once the upload succeeds, <code>onActivityForResult()<\/code> generates image thumbnails and renders them on the <code>ImageView<\/code> objects, which you defined earlier.<\/p>\n<p>In case of errors, have a look at the <a href=\"https:\/\/github.com\/Kennypee\/ImageBoard-CLoudinary\">source code<\/a>. Long code lines can be overwhelming sometimes.<\/p>\n<h2>Transformation<\/h2>\n<p>Once you have generated the image thumbnails, you can further manipulate them as any other images. For example, you generated a cropped thumbnail from the third image with a width of 200 pixels and a height of 150 pixels on the frame at 20 seconds. Also, the image is rendered in grayscale with rounded corners and a five-pixel-wide black border. Here is the code in question:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">    <span class=\"hljs-built_in\">String<\/span> thirdImgUrl = MediaManager.get().url().transformation\n                            (<span class=\"hljs-keyword\">new<\/span> Transformation().startOffset(<span class=\"hljs-string\">\"20\"<\/span>).width(<span class=\"hljs-number\">200<\/span>).height(<span class=\"hljs-number\">150<\/span>)\n                            .radius(<span class=\"hljs-number\">20<\/span>).effect(<span class=\"hljs-string\">\"grayscale\"<\/span>)\n                            .border(<span class=\"hljs-string\">\"5px_solid_black\"<\/span>).crop(<span class=\"hljs-string\">\"crop\"<\/span>)).resourceType(<span class=\"hljs-string\">\"video\"<\/span>)\n                            .format(<span class=\"hljs-string\">\"jpg\"<\/span>)\n    .generate(publicId);\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>For diversity, different manipulations apply to each of the three images. That\u2019s an excellent approach to adopt so as to institute responsiveness in images.<\/p>\n<p>This tutorial highlights only a fraction of Cloudinary\u2019s capabilities for transforming images. To learn more, see the <a href=\"https:\/\/cloudinary.com\/documentation\/image_transformations\">related section<\/a> in the Cloudinary documentation.<\/p>\n<iframe loading=\"lazy\" width=\"560\" height=\"315\" src=\"https:\/\/www.youtube.com\/embed\/tigY2I7d50c\" frameborder=\"0\" allow=\"autoplay; encrypted-media\" allowfullscreen><\/iframe>\n<h2>Conclusion<\/h2>\n<p>You are now up to speed with <a href=\"https:\/\/cloudinary.com\/users\/register_free\">Cloudinary\u2019s<\/a> outstanding capabilities for transforming images and videos. For details on Cloudinary and its vast array of useful services, check out the <a href=\"https:\/\/cloudinary.com\/documentation\">documentation<\/a>.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":41,"featured_media":21699,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[333,25,202,263,303,304],"class_list":["post-21698","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-android","tag-asset-management","tag-mobile","tag-sdk","tag-video","tag-video-transformation"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v25.6 (Yoast SEO v26.9) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>Build Your Own Image Storyboard Android App<\/title>\n<meta name=\"description\" content=\"Learn how to build storyboard apps with optimized images generated from videos on the fly by means of Cloudinary&#039;s video and image transformation features.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Building Image Storyboard Apps on Android: A Tutorial\" \/>\n<meta property=\"og:description\" content=\"Learn how to build storyboard apps with optimized images generated from videos on the fly by means of Cloudinary&#039;s video and image transformation features.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2018-04-10T17:21:37+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2024-06-03T20:20:32+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/v1649723393\/Web_Assets\/blog\/Android_Image_Storyboard_2000x1100_v2\/Android_Image_Storyboard_2000x1100_v2-jpg?_i=AA\" \/>\n\t<meta property=\"og:image:width\" content=\"1540\" \/>\n\t<meta property=\"og:image:height\" content=\"847\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"NewsArticle\",\"@id\":\"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial\"},\"author\":{\"name\":\"\",\"@id\":\"\"},\"headline\":\"Building Image Storyboard Apps on Android: A Tutorial\",\"datePublished\":\"2018-04-10T17:21:37+00:00\",\"dateModified\":\"2024-06-03T20:20:32+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial\"},\"wordCount\":8,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649723393\/Web_Assets\/blog\/Android_Image_Storyboard_2000x1100_v2\/Android_Image_Storyboard_2000x1100_v2.jpg?_i=AA\",\"keywords\":[\"Android\",\"Asset Management\",\"Mobile\",\"SDK\",\"Video\",\"Video Transformation\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2018\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial\",\"url\":\"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial\",\"name\":\"Build Your Own Image Storyboard Android App\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649723393\/Web_Assets\/blog\/Android_Image_Storyboard_2000x1100_v2\/Android_Image_Storyboard_2000x1100_v2.jpg?_i=AA\",\"datePublished\":\"2018-04-10T17:21:37+00:00\",\"dateModified\":\"2024-06-03T20:20:32+00:00\",\"description\":\"Learn how to build storyboard apps with optimized images generated from videos on the fly by means of Cloudinary's video and image transformation features.\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649723393\/Web_Assets\/blog\/Android_Image_Storyboard_2000x1100_v2\/Android_Image_Storyboard_2000x1100_v2.jpg?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649723393\/Web_Assets\/blog\/Android_Image_Storyboard_2000x1100_v2\/Android_Image_Storyboard_2000x1100_v2.jpg?_i=AA\",\"width\":1540,\"height\":847},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Building Image Storyboard Apps on Android: A Tutorial\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"name\":\"Cloudinary Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/cloudinary.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\",\"name\":\"Cloudinary Blog\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"width\":312,\"height\":60,\"caption\":\"Cloudinary Blog\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\"}},{\"@type\":\"Person\",\"@id\":\"\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Build Your Own Image Storyboard Android App","description":"Learn how to build storyboard apps with optimized images generated from videos on the fly by means of Cloudinary's video and image transformation features.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial","og_locale":"en_US","og_type":"article","og_title":"Building Image Storyboard Apps on Android: A Tutorial","og_description":"Learn how to build storyboard apps with optimized images generated from videos on the fly by means of Cloudinary's video and image transformation features.","og_url":"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial","og_site_name":"Cloudinary Blog","article_published_time":"2018-04-10T17:21:37+00:00","article_modified_time":"2024-06-03T20:20:32+00:00","og_image":[{"width":1540,"height":847,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/v1649723393\/Web_Assets\/blog\/Android_Image_Storyboard_2000x1100_v2\/Android_Image_Storyboard_2000x1100_v2-jpg?_i=AA","type":"image\/jpeg"}],"twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"NewsArticle","@id":"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial"},"author":{"name":"","@id":""},"headline":"Building Image Storyboard Apps on Android: A Tutorial","datePublished":"2018-04-10T17:21:37+00:00","dateModified":"2024-06-03T20:20:32+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial"},"wordCount":8,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649723393\/Web_Assets\/blog\/Android_Image_Storyboard_2000x1100_v2\/Android_Image_Storyboard_2000x1100_v2.jpg?_i=AA","keywords":["Android","Asset Management","Mobile","SDK","Video","Video Transformation"],"inLanguage":"en-US","copyrightYear":"2018","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial","url":"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial","name":"Build Your Own Image Storyboard Android App","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649723393\/Web_Assets\/blog\/Android_Image_Storyboard_2000x1100_v2\/Android_Image_Storyboard_2000x1100_v2.jpg?_i=AA","datePublished":"2018-04-10T17:21:37+00:00","dateModified":"2024-06-03T20:20:32+00:00","description":"Learn how to build storyboard apps with optimized images generated from videos on the fly by means of Cloudinary's video and image transformation features.","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649723393\/Web_Assets\/blog\/Android_Image_Storyboard_2000x1100_v2\/Android_Image_Storyboard_2000x1100_v2.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649723393\/Web_Assets\/blog\/Android_Image_Storyboard_2000x1100_v2\/Android_Image_Storyboard_2000x1100_v2.jpg?_i=AA","width":1540,"height":847},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/building_image_storyboard_apps_on_android_a_tutorial#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Building Image Storyboard Apps on Android: A Tutorial"}]},{"@type":"WebSite","@id":"https:\/\/cloudinary.com\/blog\/#website","url":"https:\/\/cloudinary.com\/blog\/","name":"Cloudinary Blog","description":"","publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/cloudinary.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/cloudinary.com\/blog\/#organization","name":"Cloudinary Blog","url":"https:\/\/cloudinary.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","width":312,"height":60,"caption":"Cloudinary Blog"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":""}]}},"jetpack_featured_media_url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649723393\/Web_Assets\/blog\/Android_Image_Storyboard_2000x1100_v2\/Android_Image_Storyboard_2000x1100_v2.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/21698","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/users\/41"}],"replies":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/comments?post=21698"}],"version-history":[{"count":4,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/21698\/revisions"}],"predecessor-version":[{"id":34335,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/21698\/revisions\/34335"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/21699"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=21698"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=21698"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=21698"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}