Repository: youtube/yt-direct-lite-android Branch: master Commit: 9fc42e0993a7 Files: 44 Total size: 122.9 KB Directory structure: gitextract_g6krwkb4/ ├── .classpath ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE-2.0.txt ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── libs/ │ │ └── YouTubeAndroidPlayerApi.jar │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── google/ │ │ └── ytdl/ │ │ ├── Auth.java │ │ ├── Constants.java │ │ ├── MainActivity.java │ │ ├── PlayActivity.java │ │ ├── ResumableUpload.java │ │ ├── ReviewActivity.java │ │ ├── UploadService.java │ │ ├── UploadsListFragment.java │ │ └── util/ │ │ ├── LruBitmapCache.java │ │ ├── NetworkSingleton.java │ │ ├── Upload.java │ │ ├── Utils.java │ │ └── VideoData.java │ └── res/ │ ├── drawable/ │ │ └── list_divider_horizontal_inset.xml │ ├── layout/ │ │ ├── activity_main.xml │ │ ├── activity_play.xml │ │ ├── activity_review.xml │ │ ├── developer_setup_required.xml │ │ ├── list_fragment.xml │ │ └── list_item.xml │ ├── menu/ │ │ ├── activity_main.xml │ │ ├── play.xml │ │ └── review.xml │ └── values/ │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── yt-direct-lite-android.iml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .classpath ================================================ ================================================ FILE: .gitignore ================================================ .gradle .DS_Store YouTubeDirectLiteforAndroid.iml .idea build local.properties ================================================ FILE: CONTRIBUTING.md ================================================ # How to contribute # We'd love to accept your patches and contributions to this project. There are a just a few small guidelines you need to follow. ## Contributor License Agreement ## Contributions to any Google project must be accompanied by a Contributor License Agreement. This is not a copyright **assignment**, it simply gives Google permission to use and redistribute your contributions as part of the project. * If you are an individual writing original source code and you're sure you own the intellectual property, then you'll need to sign an [individual CLA][]. * If you work for a company that wants to allow you to contribute your work, then you'll need to sign a [corporate CLA][]. You generally only need to submit a CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again. [individual CLA]: https://developers.google.com/open-source/cla/individual [corporate CLA]: https://developers.google.com/open-source/cla/corporate ## Submitting a patch ## 1. It's generally best to start by opening a new issue describing the bug or feature you're intending to fix. Even if you think it's relatively minor, it's helpful to know what people are working on. Mention in the initial issue that you are planning to work on that bug or feature so that it can be assigned to you. 1. Follow the normal process of [forking][] the project, and setup a new branch to work in. It's important that each group of changes be done in separate branches in order to ensure that a pull request only includes the commits related to that bug or feature. 1. Go makes it very simple to ensure properly formatted code, so always run `go fmt` on your code before committing it. You should also run [golint][] over your code. As noted in the [golint readme][], it's not strictly necessary that your code be completely "lint-free", but this will help you find common style issues. 1. Any significant changes should almost always be accompanied by tests. The project already has good test coverage, so look at some of the existing tests if you're unsure how to go about it. [gocov][] and [gocov-html][] are invaluable tools for seeing which parts of your code aren't being exercised by your tests. 1. Do your best to have [well-formed commit messages][] for each change. This provides consistency throughout the project, and ensures that commit messages are able to be formatted properly by various git tools. 1. Finally, push the commits to your fork and submit a [pull request][]. [forking]: https://help.github.com/articles/fork-a-repo [golint]: https://github.com/golang/lint [golint readme]: https://github.com/golang/lint/blob/master/README [gocov]: https://github.com/axw/gocov [gocov-html]: https://github.com/matm/gocov-html [well-formed commit messages]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html [squash]: http://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits [pull request]: https://help.github.com/articles/creating-a-pull-request ## Other notes on code organization ## Currently, everything is defined in the main `github` package, with API methods broken into separate service objects. These services map directly to how the [GitHub API documentation][] is organized, so use that as your guide for where to put new methods. Sub-service (e.g. [Repo Hooks][]) implementations are split into separate files based on the APIs they provide. These files are named service_api.go (e.g. repos_hooks.go) to describe the API to service mappings. [GitHub API documentation]: http://developer.github.com/v3/ [Repo Hooks]: http://developer.github.com/v3/repos/hooks/ ================================================ FILE: LICENSE-2.0.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ YouTube Direct Lite for Android =========== The code is a reference implementation for an Android OS application that captures video, uploads it to YouTube, and submits the video to a [YouTube Direct Lite](http://code.google.com/p/youtube-direct-lite/) instance. For more information, you can read the [Youtube API blog post](http://apiblog.youtube.com/2013/08/heres-my-playlist-so-submit-video-maybe.html). This application utilizes [YouTube Data API v3](https://developers.google.com/youtube/v3/) , [YouTube Android Player API](https://developers.google.com/youtube/android/player/), [YouTube Resumable Uploads](https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol?hl=en), [Google Play Services](https://developer.android.com/google/play-services/index.html) and [Plus API](https://developers.google.com/+/mobile/android/Google). To use this application, 1. In your [Google Developers Console](https://console.developers.google.com), 1. Enable the YouTube Data API v3 and Google+ API. 1. Create a client ID for Android, using your SHA1 and package name. 1. [Register your Android app](https://developers.google.com/youtube/android/player/register) 1. Plug in your Playlist Id into Constants.java and Android API Key into Auth.java ![alt tag](https://ytd-android.googlecode.com/files/YTDL.png) ![alt tag](https://ytd-android.googlecode.com/files/YTDL-review.png) ![alt tag](https://ytd-android.googlecode.com/files/YTDL-upload.png) ![alt tag](https://ytd-android.googlecode.com/files/YTDL-watch.png) ================================================ FILE: app/.gitignore ================================================ /build app.iml ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' android { compileSdkVersion 23 buildToolsVersion "23.0.0" defaultConfig { applicationId "com.google.ytdl" minSdkVersion 16 targetSdkVersion 23 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.google.android.gms:play-services-plus:7.8.0' compile 'com.android.support:support-v13:23.0.0' compile 'com.google.apis:google-api-services-youtube:v3-rev120-1.19.0' compile 'com.google.http-client:google-http-client-android:+' compile 'com.google.api-client:google-api-client-android:+' compile 'com.google.api-client:google-api-client-gson:+' compile 'com.google.code.gson:gson:2.2.4' compile 'com.mcxiaoke.volley:library:1.0.18' compile files('libs/YouTubeAndroidPlayerApi.jar') } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Users/ulukaya/android-sdks/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/google/ytdl/Auth.java ================================================ /* * Copyright (c) 2013 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.ytdl; import com.google.android.gms.common.Scopes; import com.google.api.services.youtube.YouTubeScopes; public class Auth { // Register an API key here: https://console.developers.google.com public static final String KEY = "Replace me with your API key"; public static final String[] SCOPES = {Scopes.PROFILE, YouTubeScopes.YOUTUBE}; } ================================================ FILE: app/src/main/java/com/google/ytdl/Constants.java ================================================ /* * Copyright (c) 2013 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.ytdl; /** * @author Ibrahim Ulukaya *

* This class hold constants. */ public class Constants { public static final int MAX_KEYWORD_LENGTH = 30; public static final String DEFAULT_KEYWORD = "ytdl"; // A playlist ID is a string that begins with PL. You must replace this string with the correct // playlist ID for the app to work public static final String UPLOAD_PLAYLIST = "Replace me with the playlist ID you want to upload into"; public static final String APP_NAME = "ytd-android"; } ================================================ FILE: app/src/main/java/com/google/ytdl/MainActivity.java ================================================ /* * Copyright (c) 2013 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.ytdl; import android.accounts.AccountManager; import android.app.Activity; import android.app.Dialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.preference.PreferenceManager; import android.provider.MediaStore; import android.support.v4.content.LocalBroadcastManager; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.widget.ArrayAdapter; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.android.volley.toolbox.ImageLoader; import com.google.android.gms.common.GooglePlayServicesUtil; import com.google.api.client.extensions.android.http.AndroidHttp; import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential; import com.google.api.client.googleapis.extensions.android.gms.auth.GooglePlayServicesAvailabilityIOException; import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException; import com.google.api.client.http.HttpTransport; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.gson.GsonFactory; import com.google.api.client.util.ExponentialBackOff; import com.google.api.services.youtube.YouTube; import com.google.api.services.youtube.model.ChannelListResponse; import com.google.api.services.youtube.model.PlaylistItem; import com.google.api.services.youtube.model.PlaylistItemListResponse; import com.google.api.services.youtube.model.Video; import com.google.api.services.youtube.model.VideoListResponse; import com.google.api.services.youtube.model.VideoSnippet; import com.google.ytdl.util.NetworkSingleton; import com.google.ytdl.util.Upload; import com.google.ytdl.util.Utils; import com.google.ytdl.util.VideoData; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; /** * @author Ibrahim Ulukaya *

* Main activity class which handles authorization and intents. */ public class MainActivity extends Activity implements UploadsListFragment.Callbacks { // private static final int MEDIA_TYPE_VIDEO = 7; public static final String ACCOUNT_KEY = "accountName"; public static final String MESSAGE_KEY = "message"; public static final String YOUTUBE_ID = "youtubeId"; public static final String YOUTUBE_WATCH_URL_PREFIX = "http://www.youtube.com/watch?v="; static final String REQUEST_AUTHORIZATION_INTENT = "com.google.example.yt.RequestAuth"; static final String REQUEST_AUTHORIZATION_INTENT_PARAM = "com.google.example.yt.RequestAuth.param"; private static final int REQUEST_GOOGLE_PLAY_SERVICES = 0; private static final int REQUEST_GMS_ERROR_DIALOG = 1; private static final int REQUEST_ACCOUNT_PICKER = 2; private static final int REQUEST_AUTHORIZATION = 3; private static final int RESULT_PICK_IMAGE_CROP = 4; private static final int RESULT_VIDEO_CAP = 5; private static final int REQUEST_DIRECT_TAG = 6; private static final String TAG = "MainActivity"; final HttpTransport transport = AndroidHttp.newCompatibleTransport(); final JsonFactory jsonFactory = new GsonFactory(); GoogleAccountCredential credential; private ImageLoader mImageLoader; private String mChosenAccountName; private Uri mFileURI = null; private VideoData mVideoData; private UploadBroadcastReceiver broadcastReceiver; private UploadsListFragment mUploadsListFragment; @Override protected void onCreate(Bundle savedInstanceState) { getWindow().requestFeature(Window.FEATURE_INDETERMINATE_PROGRESS); super.onCreate(savedInstanceState); mUploadsListFragment = new UploadsListFragment(getApplicationContext()); // Check to see if the proper keys and playlist IDs have been set up if (!isCorrectlyConfigured()) { setContentView(R.layout.developer_setup_required); showMissingConfigurations(); } else { setContentView(R.layout.activity_main); ensureLoader(); credential = GoogleAccountCredential.usingOAuth2( getApplicationContext(), Arrays.asList(Auth.SCOPES)); // set exponential backoff policy credential.setBackOff(new ExponentialBackOff()); if (savedInstanceState != null) { mChosenAccountName = savedInstanceState.getString(ACCOUNT_KEY); } else { loadAccount(); } credential.setSelectedAccountName(mChosenAccountName); mUploadsListFragment = (UploadsListFragment) getFragmentManager() .findFragmentById(R.id.list_fragment); } } /** * This method checks various internal states to figure out at startup time * whether certain elements have been configured correctly by the developer. * Checks that: *

* * @return true if the application is correctly configured for use, false if * not */ private boolean isCorrectlyConfigured() { // This isn't going to internationalize well, but we only really need // this for the sample app. // Real applications will remove this section of code and ensure that // all of these values are configured. if (Auth.KEY.startsWith("Replace")) { return false; } if (Constants.UPLOAD_PLAYLIST.startsWith("Replace")) { return false; } return true; } /** * This method renders the ListView explaining what the configurations the * developer of this application has to complete. Typically, these are * static variables defined in {@link Auth} and {@link Constants}. */ private void showMissingConfigurations() { List missingConfigs = new ArrayList(); // Make sure an API key is registered if (Auth.KEY.startsWith("Replace")) { missingConfigs .add(new MissingConfig( "API key not configured", "KEY constant in Auth.java must be configured with your Simple API key from the Google API Console")); } // Make sure a playlist ID is registered if (Constants.UPLOAD_PLAYLIST.startsWith("Replace")) { missingConfigs .add(new MissingConfig( "Playlist ID not configured", "UPLOAD_PLAYLIST constant in Constants.java must be configured with a Playlist ID to submit to. (The playlist ID typically has a prexix of PL)")); } // Renders a simple_list_item_2, which consists of a title and a body // element ListAdapter adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_2, missingConfigs) { @Override public View getView(int position, View convertView, ViewGroup parent) { View row; if (convertView == null) { LayoutInflater inflater = (LayoutInflater) getApplicationContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); row = inflater.inflate(android.R.layout.simple_list_item_2, null); } else { row = convertView; } TextView titleView = (TextView) row .findViewById(android.R.id.text1); TextView bodyView = (TextView) row .findViewById(android.R.id.text2); MissingConfig config = getItem(position); titleView.setText(config.title); bodyView.setText(config.body); return row; } }; // Wire the data adapter up to the view ListView missingConfigList = (ListView) findViewById(R.id.missing_config_list); missingConfigList.setAdapter(adapter); } @Override protected void onResume() { super.onResume(); if (broadcastReceiver == null) broadcastReceiver = new UploadBroadcastReceiver(); IntentFilter intentFilter = new IntentFilter( REQUEST_AUTHORIZATION_INTENT); LocalBroadcastManager.getInstance(this).registerReceiver( broadcastReceiver, intentFilter); } private void ensureLoader() { if (mImageLoader == null) { // Get the ImageLoader through your singleton class. mImageLoader = NetworkSingleton.getInstance(this).getImageLoader(); } } private void loadAccount() { SharedPreferences sp = PreferenceManager .getDefaultSharedPreferences(this); mChosenAccountName = sp.getString(ACCOUNT_KEY, null); invalidateOptionsMenu(); } private void saveAccount() { SharedPreferences sp = PreferenceManager .getDefaultSharedPreferences(this); sp.edit().putString(ACCOUNT_KEY, mChosenAccountName).commit(); } private void loadData() { if (mChosenAccountName == null) { return; } loadUploadedVideos(); } @Override protected void onPause() { super.onPause(); if (broadcastReceiver != null) { LocalBroadcastManager.getInstance(this).unregisterReceiver( broadcastReceiver); } if (isFinishing()) { // mHandler.removeCallbacksAndMessages(null); } } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.activity_main, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_refresh: loadData(); break; case R.id.menu_accounts: chooseAccount(); return true; } return super.onOptionsItemSelected(item); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case REQUEST_GMS_ERROR_DIALOG: break; case RESULT_PICK_IMAGE_CROP: if (resultCode == RESULT_OK) { mFileURI = data.getData(); if (mFileURI != null) { Intent intent = new Intent(this, ReviewActivity.class); intent.setData(mFileURI); startActivity(intent); } } break; case RESULT_VIDEO_CAP: if (resultCode == RESULT_OK) { mFileURI = data.getData(); if (mFileURI != null) { Intent intent = new Intent(this, ReviewActivity.class); intent.setData(mFileURI); startActivity(intent); } } break; case REQUEST_GOOGLE_PLAY_SERVICES: if (resultCode == Activity.RESULT_OK) { haveGooglePlayServices(); } else { checkGooglePlayServicesAvailable(); } break; case REQUEST_AUTHORIZATION: if (resultCode != Activity.RESULT_OK) { chooseAccount(); } break; case REQUEST_ACCOUNT_PICKER: if (resultCode == Activity.RESULT_OK && data != null && data.getExtras() != null) { String accountName = data.getExtras().getString( AccountManager.KEY_ACCOUNT_NAME); if (accountName != null) { mChosenAccountName = accountName; credential.setSelectedAccountName(accountName); saveAccount(); } } break; case REQUEST_DIRECT_TAG: if (resultCode == Activity.RESULT_OK && data != null && data.getExtras() != null) { String youtubeId = data.getStringExtra(YOUTUBE_ID); if (youtubeId.equals(mVideoData.getYouTubeId())) { directTag(mVideoData); } } break; } } private void directTag(final VideoData video) { final Video updateVideo = new Video(); VideoSnippet snippet = video .addTags(Arrays.asList( Constants.DEFAULT_KEYWORD, Upload.generateKeywordFromPlaylistId(Constants.UPLOAD_PLAYLIST))); updateVideo.setSnippet(snippet); updateVideo.setId(video.getYouTubeId()); new AsyncTask() { @Override protected Void doInBackground(Void... voids) { YouTube youtube = new YouTube.Builder(transport, jsonFactory, credential).setApplicationName(Constants.APP_NAME) .build(); try { youtube.videos().update("snippet", updateVideo).execute(); } catch (UserRecoverableAuthIOException e) { startActivityForResult(e.getIntent(), REQUEST_AUTHORIZATION); } catch (IOException e) { Log.e(TAG, e.getMessage()); } return null; } }.execute((Void) null); Toast.makeText(this, R.string.video_submitted_to_ytdl, Toast.LENGTH_LONG) .show(); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString(ACCOUNT_KEY, mChosenAccountName); } private void loadUploadedVideos() { if (mChosenAccountName == null) { return; } setProgressBarIndeterminateVisibility(true); new AsyncTask>() { @Override protected List doInBackground(Void... voids) { YouTube youtube = new YouTube.Builder(transport, jsonFactory, credential).setApplicationName(Constants.APP_NAME) .build(); try { /* * Now that the user is authenticated, the app makes a * channels list request to get the authenticated user's * channel. Returned with that data is the playlist id for * the uploaded videos. * https://developers.google.com/youtube * /v3/docs/channels/list */ ChannelListResponse clr = youtube.channels() .list("contentDetails").setMine(true).execute(); // Get the user's uploads playlist's id from channel list // response String uploadsPlaylistId = clr.getItems().get(0) .getContentDetails().getRelatedPlaylists() .getUploads(); List videos = new ArrayList(); // Get videos from user's upload playlist with a playlist // items list request PlaylistItemListResponse pilr = youtube.playlistItems() .list("id,contentDetails") .setPlaylistId(uploadsPlaylistId) .setMaxResults(20l).execute(); List videoIds = new ArrayList(); // Iterate over playlist item list response to get uploaded // videos' ids. for (PlaylistItem item : pilr.getItems()) { videoIds.add(item.getContentDetails().getVideoId()); } // Get details of uploaded videos with a videos list // request. VideoListResponse vlr = youtube.videos() .list("id,snippet,status") .setId(TextUtils.join(",", videoIds)).execute(); // Add only the public videos to the local videos list. for (Video video : vlr.getItems()) { if ("public".equals(video.getStatus() .getPrivacyStatus())) { VideoData videoData = new VideoData(); videoData.setVideo(video); videos.add(videoData); } } // Sort videos by title Collections.sort(videos, new Comparator() { @Override public int compare(VideoData videoData, VideoData videoData2) { return videoData.getTitle().compareTo( videoData2.getTitle()); } }); return videos; } catch (final GooglePlayServicesAvailabilityIOException availabilityException) { showGooglePlayServicesAvailabilityErrorDialog(availabilityException .getConnectionStatusCode()); } catch (UserRecoverableAuthIOException userRecoverableException) { startActivityForResult( userRecoverableException.getIntent(), REQUEST_AUTHORIZATION); } catch (IOException e) { Utils.logAndShow(MainActivity.this, Constants.APP_NAME, e); } return null; } @Override protected void onPostExecute(List videos) { setProgressBarIndeterminateVisibility(false); if (videos == null) { return; } mUploadsListFragment.setVideos(videos); } }.execute((Void) null); } @Override public void onBackPressed() { // if (mDirectFragment.popPlayerFromBackStack()) { // super.onBackPressed(); // } } @Override public ImageLoader onGetImageLoader() { ensureLoader(); return mImageLoader; } @Override public void onVideoSelected(VideoData video) { mVideoData = video; Intent intent = new Intent(this, PlayActivity.class); intent.putExtra(YOUTUBE_ID, video.getYouTubeId()); startActivityForResult(intent, REQUEST_DIRECT_TAG); } @Override public void onConnected(String connectedAccountName) { // Make API requests only when the user has successfully signed in. loadData(); } public void pickFile(View view) { Intent intent = new Intent(Intent.ACTION_PICK); intent.setType("video/*"); startActivityForResult(intent, RESULT_PICK_IMAGE_CROP); } public void recordVideo(View view) { Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); // Workaround for Nexus 7 Android 4.3 Intent Returning Null problem // create a file to save the video in specific folder (this works for // video only) // mFileURI = getOutputMediaFile(MEDIA_TYPE_VIDEO); // intent.putExtra(MediaStore.EXTRA_OUTPUT, mFileURI); // set the video image quality to high intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1); // start the Video Capture Intent startActivityForResult(intent, RESULT_VIDEO_CAP); } public void showGooglePlayServicesAvailabilityErrorDialog( final int connectionStatusCode) { runOnUiThread(new Runnable() { public void run() { Dialog dialog = GooglePlayServicesUtil.getErrorDialog( connectionStatusCode, MainActivity.this, REQUEST_GOOGLE_PLAY_SERVICES); dialog.show(); } }); } /** * Check that Google Play services APK is installed and up to date. */ private boolean checkGooglePlayServicesAvailable() { final int connectionStatusCode = GooglePlayServicesUtil .isGooglePlayServicesAvailable(this); if (GooglePlayServicesUtil.isUserRecoverableError(connectionStatusCode)) { showGooglePlayServicesAvailabilityErrorDialog(connectionStatusCode); return false; } return true; } private void haveGooglePlayServices() { // check if there is already an account selected if (credential.getSelectedAccountName() == null) { // ask user to choose account chooseAccount(); } } private void chooseAccount() { startActivityForResult(credential.newChooseAccountIntent(), REQUEST_ACCOUNT_PICKER); } /** * Private class representing a missing configuration and what the developer * can do to fix the issue. */ private class MissingConfig { public final String title; public final String body; public MissingConfig(String title, String body) { this.title = title; this.body = body; } } // public Uri getOutputMediaFile(int type) // { // // To be safe, you should check that the SDCard is mounted // if(Environment.getExternalStorageState() != null) { // // this works for Android 2.2 and above // File mediaStorageDir = new // File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), // "SMW_VIDEO"); // // // This location works best if you want the created images to be shared // // between applications and persist after your app has been uninstalled. // // // Create the storage directory if it does not exist // if (! mediaStorageDir.exists()) { // if (! mediaStorageDir.mkdirs()) { // Log.d(TAG, "failed to create directory"); // return null; // } // } // // // Create a media file name // String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", // Locale.getDefault()).format(new Date()); // File mediaFile; // if(type == MEDIA_TYPE_VIDEO) { // mediaFile = new File(mediaStorageDir.getPath() + File.separator + // "VID_"+ timeStamp + ".mp4"); // } else { // return null; // } // // return Uri.fromFile(mediaFile); // } // // return null; // } private class UploadBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(REQUEST_AUTHORIZATION_INTENT)) { Log.d(TAG, "Request auth received - executing the intent"); Intent toRun = intent .getParcelableExtra(REQUEST_AUTHORIZATION_INTENT_PARAM); startActivityForResult(toRun, REQUEST_AUTHORIZATION); } } } } ================================================ FILE: app/src/main/java/com/google/ytdl/PlayActivity.java ================================================ package com.google.ytdl; import android.app.Activity; import android.app.FragmentTransaction; import android.content.Intent; import android.os.Bundle; import android.support.v4.app.NavUtils; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Button; import android.widget.Toast; import com.google.android.youtube.player.YouTubeInitializationResult; import com.google.android.youtube.player.YouTubePlayer; import com.google.android.youtube.player.YouTubePlayer.OnFullscreenListener; import com.google.android.youtube.player.YouTubePlayer.PlayerStateChangeListener; import com.google.android.youtube.player.YouTubePlayerFragment; import com.google.api.client.extensions.android.http.AndroidHttp; import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential; import com.google.api.client.http.HttpTransport; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.gson.GsonFactory; import com.google.ytdl.util.VideoData; /* * Copyright (c) 2013 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ /** * @author Ibrahim Ulukaya *

* Main fragment showing YouTube Direct Lite upload options and having * YT Android Player. */ public class PlayActivity extends Activity implements PlayerStateChangeListener, OnFullscreenListener { private static final String YOUTUBE_FRAGMENT_TAG = "youtube"; final HttpTransport transport = AndroidHttp.newCompatibleTransport(); final JsonFactory jsonFactory = new GsonFactory(); GoogleAccountCredential credential; private YouTubePlayer mYouTubePlayer; private boolean mIsFullScreen = false; private Intent intent; public PlayActivity() { } @Override public void onStart() { super.onStart(); } @Override public void onStop() { super.onStop(); } public void directLite(View view) { this.setResult(RESULT_OK, intent); finish(); } public void panToVideo(final String youtubeId) { popPlayerFromBackStack(); YouTubePlayerFragment playerFragment = YouTubePlayerFragment .newInstance(); getFragmentManager() .beginTransaction() .replace(R.id.detail_container, playerFragment, YOUTUBE_FRAGMENT_TAG) .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .addToBackStack(null).commit(); playerFragment.initialize(Auth.KEY, new YouTubePlayer.OnInitializedListener() { @Override public void onInitializationSuccess( YouTubePlayer.Provider provider, YouTubePlayer youTubePlayer, boolean b) { youTubePlayer.loadVideo(youtubeId); mYouTubePlayer = youTubePlayer; youTubePlayer .setPlayerStateChangeListener(PlayActivity.this); youTubePlayer .setOnFullscreenListener(PlayActivity.this); } @Override public void onInitializationFailure( YouTubePlayer.Provider provider, YouTubeInitializationResult result) { showErrorToast(result.toString()); } }); } public boolean popPlayerFromBackStack() { if (mIsFullScreen) { mYouTubePlayer.setFullscreen(false); return false; } if (getFragmentManager().findFragmentByTag(YOUTUBE_FRAGMENT_TAG) != null) { getFragmentManager().popBackStack(); return false; } return true; } @Override public void onAdStarted() { } @Override public void onError(YouTubePlayer.ErrorReason errorReason) { showErrorToast(errorReason.toString()); } private void showErrorToast(String message) { Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT) .show(); } @Override public void onLoaded(String arg0) { } @Override public void onLoading() { } @Override public void onVideoEnded() { // popPlayerFromBackStack(); } @Override public void onVideoStarted() { } @Override public void onFullscreen(boolean fullScreen) { mIsFullScreen = fullScreen; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getActionBar().setDisplayHomeAsUpEnabled(true); setContentView(R.layout.activity_play); intent = getIntent(); Button submitButton = (Button) findViewById(R.id.submit_button); if (Intent.ACTION_VIEW.equals(intent.getAction())) { submitButton.setVisibility(View.GONE); setTitle(R.string.playing_uploaded_video); } String youtubeId = intent.getStringExtra(MainActivity.YOUTUBE_ID); panToVideo(youtubeId); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.play, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { // Respond to the action bar's Up/Home button case android.R.id.home: NavUtils.navigateUpFromSameTask(this); return true; } return super.onOptionsItemSelected(item); } @Override public void onBackPressed() { super.onBackPressed(); NavUtils.navigateUpFromSameTask(this); } public interface Callbacks { public void onVideoSelected(VideoData video); public void onResume(); } } ================================================ FILE: app/src/main/java/com/google/ytdl/ResumableUpload.java ================================================ /* * Copyright (c) 2013 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.ytdl; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.ThumbnailUtils; import android.net.Uri; import android.provider.MediaStore.Video.Thumbnails; import android.support.v4.app.NotificationCompat; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import com.google.api.client.googleapis.extensions.android.gms.auth.GooglePlayServicesAvailabilityIOException; import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException; import com.google.api.client.googleapis.media.MediaHttpUploader; import com.google.api.client.googleapis.media.MediaHttpUploaderProgressListener; import com.google.api.client.http.InputStreamContent; import com.google.api.services.youtube.YouTube; import com.google.api.services.youtube.model.Video; import com.google.api.services.youtube.model.VideoListResponse; import com.google.api.services.youtube.model.VideoSnippet; import com.google.api.services.youtube.model.VideoStatus; import com.google.ytdl.util.Upload; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; import java.util.Calendar; import java.util.List; /** * @author Ibrahim Ulukaya *

* YouTube Resumable Upload controller class. */ public class ResumableUpload { /** * Assigned to the upload */ public static final String[] DEFAULT_KEYWORDS = {"MultiSquash", "Game"}; /** * Indicates that the video is fully processed, see https://www.googleapis.com/discovery/v1/apis/youtube/v3/rpc */ private static final String SUCCEEDED = "succeeded"; private static final String TAG = "UploadingActivity"; private static int UPLOAD_NOTIFICATION_ID = 1001; private static int PLAYBACK_NOTIFICATION_ID = 1002; /* * Global instance of the format used for the video being uploaded (MIME type). */ private static String VIDEO_FILE_FORMAT = "video/*"; /** * Uploads user selected video in the project folder to the user's YouTube account using OAuth2 * for authentication. */ public static String upload(YouTube youtube, final InputStream fileInputStream, final long fileSize, final Uri mFileUri, final String path, final Context context) { final NotificationManager notifyManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); final NotificationCompat.Builder builder = new NotificationCompat.Builder(context); Intent notificationIntent = new Intent(context, ReviewActivity.class); notificationIntent.setData(mFileUri); notificationIntent.setAction(Intent.ACTION_VIEW); Bitmap thumbnail = ThumbnailUtils.createVideoThumbnail(path, Thumbnails.MICRO_KIND); PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT); builder.setContentTitle(context.getString(R.string.youtube_upload)) .setContentText(context.getString(R.string.youtube_upload_started)) .setSmallIcon(R.drawable.ic_stat_device_access_video).setContentIntent(contentIntent).setStyle(new NotificationCompat.BigPictureStyle().bigPicture(thumbnail)); notifyManager.notify(UPLOAD_NOTIFICATION_ID, builder.build()); String videoId = null; try { // Add extra information to the video before uploading. Video videoObjectDefiningMetadata = new Video(); /* * Set the video to public, so it is available to everyone (what most people want). This is * actually the default, but I wanted you to see what it looked like in case you need to set * it to "unlisted" or "private" via API. */ VideoStatus status = new VideoStatus(); status.setPrivacyStatus("public"); videoObjectDefiningMetadata.setStatus(status); // We set a majority of the metadata with the VideoSnippet object. VideoSnippet snippet = new VideoSnippet(); /* * The Calendar instance is used to create a unique name and description for test purposes, so * you can see multiple files being uploaded. You will want to remove this from your project * and use your own standard names. */ Calendar cal = Calendar.getInstance(); snippet.setTitle("Test Upload via Java on " + cal.getTime()); snippet.setDescription("Video uploaded via YouTube Data API V3 using the Java library " + "on " + cal.getTime()); // Set your keywords. snippet.setTags(Arrays.asList(Constants.DEFAULT_KEYWORD, Upload.generateKeywordFromPlaylistId(Constants.UPLOAD_PLAYLIST))); // Set completed snippet to the video object. videoObjectDefiningMetadata.setSnippet(snippet); InputStreamContent mediaContent = new InputStreamContent(VIDEO_FILE_FORMAT, new BufferedInputStream(fileInputStream)); mediaContent.setLength(fileSize); /* * The upload command includes: 1. Information we want returned after file is successfully * uploaded. 2. Metadata we want associated with the uploaded video. 3. Video file itself. */ YouTube.Videos.Insert videoInsert = youtube.videos().insert("snippet,statistics,status", videoObjectDefiningMetadata, mediaContent); // Set the upload type and add event listener. MediaHttpUploader uploader = videoInsert.getMediaHttpUploader(); /* * Sets whether direct media upload is enabled or disabled. True = whole media content is * uploaded in a single request. False (default) = resumable media upload protocol to upload * in data chunks. */ uploader.setDirectUploadEnabled(false); MediaHttpUploaderProgressListener progressListener = new MediaHttpUploaderProgressListener() { public void progressChanged(MediaHttpUploader uploader) throws IOException { switch (uploader.getUploadState()) { case INITIATION_STARTED: builder.setContentText(context.getString(R.string.initiation_started)).setProgress((int) fileSize, (int) uploader.getNumBytesUploaded(), false); notifyManager.notify(UPLOAD_NOTIFICATION_ID, builder.build()); break; case INITIATION_COMPLETE: builder.setContentText(context.getString(R.string.initiation_completed)).setProgress((int) fileSize, (int) uploader.getNumBytesUploaded(), false); notifyManager.notify(UPLOAD_NOTIFICATION_ID, builder.build()); break; case MEDIA_IN_PROGRESS: builder .setContentTitle(context.getString(R.string.youtube_upload) + (int) (uploader.getProgress() * 100) + "%") .setContentText(context.getString(R.string.upload_in_progress)) .setProgress((int) fileSize, (int) uploader.getNumBytesUploaded(), false); notifyManager.notify(UPLOAD_NOTIFICATION_ID, builder.build()); break; case MEDIA_COMPLETE: builder.setContentTitle(context.getString(R.string.yt_upload_completed)) .setContentText(context.getString(R.string.upload_completed)) // Removes the progress bar .setProgress(0, 0, false); notifyManager.notify(UPLOAD_NOTIFICATION_ID, builder.build()); case NOT_STARTED: Log.d(this.getClass().getSimpleName(), context.getString(R.string.upload_not_started)); break; } } }; uploader.setProgressListener(progressListener); // Execute upload. Video returnedVideo = videoInsert.execute(); Log.d(TAG, "Video upload completed"); videoId = returnedVideo.getId(); Log.d(TAG, String.format("videoId = [%s]", videoId)); } catch (final GooglePlayServicesAvailabilityIOException availabilityException) { Log.e(TAG, "GooglePlayServicesAvailabilityIOException", availabilityException); notifyFailedUpload(context, context.getString(R.string.cant_access_play), notifyManager, builder); } catch (UserRecoverableAuthIOException userRecoverableException) { Log.i(TAG, String.format("UserRecoverableAuthIOException: %s", userRecoverableException.getMessage())); requestAuth(context, userRecoverableException); } catch (IOException e) { Log.e(TAG, "IOException", e); notifyFailedUpload(context, context.getString(R.string.please_try_again), notifyManager, builder); } return videoId; } private static void requestAuth(Context context, UserRecoverableAuthIOException userRecoverableException) { LocalBroadcastManager manager = LocalBroadcastManager.getInstance(context); Intent authIntent = userRecoverableException.getIntent(); Intent runReqAuthIntent = new Intent(MainActivity.REQUEST_AUTHORIZATION_INTENT); runReqAuthIntent.putExtra(MainActivity.REQUEST_AUTHORIZATION_INTENT_PARAM, authIntent); manager.sendBroadcast(runReqAuthIntent); Log.d(TAG, String.format("Sent broadcast %s", MainActivity.REQUEST_AUTHORIZATION_INTENT)); } private static void notifyFailedUpload(Context context, String message, NotificationManager notifyManager, NotificationCompat.Builder builder) { builder.setContentTitle(context.getString(R.string.yt_upload_failed)) .setContentText(message); notifyManager.notify(UPLOAD_NOTIFICATION_ID, builder.build()); Log.e(ResumableUpload.class.getSimpleName(), message); } public static void showSelectableNotification(String videoId, Context context) { Log.d(TAG, String.format("Posting selectable notification for video ID [%s]", videoId)); final NotificationManager notifyManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); final NotificationCompat.Builder builder = new NotificationCompat.Builder(context); Intent notificationIntent = new Intent(context, PlayActivity.class); notificationIntent.putExtra(MainActivity.YOUTUBE_ID, videoId); notificationIntent.setAction(Intent.ACTION_VIEW); URL url; try { url = new URL("https://i1.ytimg.com/vi/" + videoId + "/mqdefault.jpg"); Bitmap thumbnail = BitmapFactory.decodeStream(url.openConnection().getInputStream()); PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT); builder.setContentTitle(context.getString(R.string.watch_your_video)) .setContentText(context.getString(R.string.see_the_newly_uploaded_video)).setContentIntent(contentIntent).setSmallIcon(R.drawable.ic_stat_device_access_video).setStyle(new NotificationCompat.BigPictureStyle().bigPicture(thumbnail)); notifyManager.notify(PLAYBACK_NOTIFICATION_ID, builder.build()); Log.d(TAG, String.format("Selectable notification for video ID [%s] posted", videoId)); } catch (MalformedURLException e) { Log.e(TAG, e.getMessage()); } catch (IOException e) { Log.e(TAG, e.getMessage()); } } /** * @return url of thumbnail if the video is fully processed */ public static boolean checkIfProcessed(String videoId, YouTube youtube) { try { YouTube.Videos.List list = youtube.videos().list("processingDetails"); list.setId(videoId); VideoListResponse listResponse = list.execute(); List

* Intent service to handle uploads. */ public class UploadService extends IntentService { /** * defines how long we'll wait for a video to finish processing */ private static final int PROCESSING_TIMEOUT_SEC = 60 * 20; // 20 minutes /** * controls how often to poll for video processing status */ private static final int PROCESSING_POLL_INTERVAL_SEC = 60; /** * how long to wait before re-trying the upload */ private static final int UPLOAD_REATTEMPT_DELAY_SEC = 60; /** * max number of retry attempts */ private static final int MAX_RETRY = 3; private static final String TAG = "UploadService"; /** * processing start time */ private static long mStartTime; final HttpTransport transport = AndroidHttp.newCompatibleTransport(); final JsonFactory jsonFactory = new GsonFactory(); GoogleAccountCredential credential; /** * tracks the number of upload attempts */ private int mUploadAttemptCount; public UploadService() { super("YTUploadService"); } private static void zzz(int duration) throws InterruptedException { Log.d(TAG, String.format("Sleeping for [%d] ms ...", duration)); Thread.sleep(duration); Log.d(TAG, String.format("Sleeping for [%d] ms ... done", duration)); } private static boolean timeoutExpired(long startTime, int timeoutSeconds) { long currTime = System.currentTimeMillis(); long elapsed = currTime - startTime; if (elapsed >= timeoutSeconds * 1000) { return true; } else { return false; } } @Override protected void onHandleIntent(Intent intent) { Uri fileUri = intent.getData(); String chosenAccountName = intent.getStringExtra(MainActivity.ACCOUNT_KEY); credential = GoogleAccountCredential.usingOAuth2(getApplicationContext(), Lists.newArrayList(Auth.SCOPES)); credential.setSelectedAccountName(chosenAccountName); credential.setBackOff(new ExponentialBackOff()); String appName = getResources().getString(R.string.app_name); final YouTube youtube = new YouTube.Builder(transport, jsonFactory, credential).setApplicationName( appName).build(); try { tryUploadAndShowSelectableNotification(fileUri, youtube); } catch (InterruptedException e) { // ignore } } private void tryUploadAndShowSelectableNotification(final Uri fileUri, final YouTube youtube) throws InterruptedException { while (true) { Log.i(TAG, String.format("Uploading [%s] to YouTube", fileUri.toString())); String videoId = tryUpload(fileUri, youtube); if (videoId != null) { Log.i(TAG, String.format("Uploaded video with ID: %s", videoId)); tryShowSelectableNotification(videoId, youtube); return; } else { Log.e(TAG, String.format("Failed to upload %s", fileUri.toString())); if (mUploadAttemptCount++ < MAX_RETRY) { Log.i(TAG, String.format("Will retry to upload the video ([%d] out of [%d] reattempts)", mUploadAttemptCount, MAX_RETRY)); zzz(UPLOAD_REATTEMPT_DELAY_SEC * 1000); } else { Log.e(TAG, String.format("Giving up on trying to upload %s after %d attempts", fileUri.toString(), mUploadAttemptCount)); return; } } } } private void tryShowSelectableNotification(final String videoId, final YouTube youtube) throws InterruptedException { mStartTime = System.currentTimeMillis(); boolean processed = false; while (!processed) { processed = ResumableUpload.checkIfProcessed(videoId, youtube); if (!processed) { // wait a while Log.d(TAG, String.format("Video [%s] is not processed yet, will retry after [%d] seconds", videoId, PROCESSING_POLL_INTERVAL_SEC)); if (!timeoutExpired(mStartTime, PROCESSING_TIMEOUT_SEC)) { zzz(PROCESSING_POLL_INTERVAL_SEC * 1000); } else { Log.d(TAG, String.format("Bailing out polling for processing status after [%d] seconds", PROCESSING_TIMEOUT_SEC)); return; } } else { ResumableUpload.showSelectableNotification(videoId, getApplicationContext()); return; } } } private String tryUpload(Uri mFileUri, YouTube youtube) { long fileSize; InputStream fileInputStream = null; String videoId = null; try { fileSize = getContentResolver().openFileDescriptor(mFileUri, "r").getStatSize(); fileInputStream = getContentResolver().openInputStream(mFileUri); String[] proj = {MediaStore.Images.Media.DATA}; Cursor cursor = getContentResolver().query(mFileUri, proj, null, null, null); int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); cursor.moveToFirst(); videoId = ResumableUpload.upload(youtube, fileInputStream, fileSize, mFileUri, cursor.getString(column_index), getApplicationContext()); } catch (FileNotFoundException e) { Log.e(getApplicationContext().toString(), e.getMessage()); } finally { try { fileInputStream.close(); } catch (IOException e) { // ignore } } return videoId; } } ================================================ FILE: app/src/main/java/com/google/ytdl/UploadsListFragment.java ================================================ /* * Copyright (c) 2013 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.ytdl; import android.annotation.SuppressLint; import android.app.Activity; import android.app.Fragment; import android.content.Context; import android.content.IntentSender; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.GridView; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import com.android.volley.toolbox.ImageLoader; import com.android.volley.toolbox.NetworkImageView; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener; import com.google.android.gms.plus.Plus; import com.google.android.gms.plus.PlusOneButton; import com.google.android.gms.plus.model.people.Person; import com.google.ytdl.util.VideoData; import java.util.List; /** * @author Ibrahim Ulukaya *

* Left side fragment showing user's uploaded YouTube videos. */ public class UploadsListFragment extends Fragment implements ConnectionCallbacks, OnConnectionFailedListener { private static final String TAG = UploadsListFragment.class.getName(); private static Context mContext; private Callbacks mCallbacks; private GoogleApiClient mGoogleApiClient; private GridView mGridView; private ImageLoader mImageLoader; public UploadsListFragment() { } @SuppressLint("ValidFragment") public UploadsListFragment(Context context) { mContext = context; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mGoogleApiClient = new GoogleApiClient.Builder(mContext) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .addApi(Plus.API) .addScope(Plus.SCOPE_PLUS_PROFILE) .build(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View listView = inflater.inflate(R.layout.list_fragment, container, false); mGridView = (GridView) listView.findViewById(R.id.grid_view); TextView emptyView = (TextView) listView.findViewById(android.R.id.empty); mGridView.setEmptyView(emptyView); return listView; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); setProfileInfo(); } public void setVideos(List videos) { if (!isAdded()) { return; } mGridView.setAdapter(new UploadedVideoAdapter(videos)); } public void setProfileInfo() { //not sure if mGoogleapiClient.isConnect is appropriate... if (!mGoogleApiClient.isConnected() || Plus.PeopleApi.getCurrentPerson(mGoogleApiClient) == null) { ((ImageView) getView().findViewById(R.id.avatar)) .setImageDrawable(null); ((TextView) getView().findViewById(R.id.display_name)) .setText(R.string.not_signed_in); } else { Person currentPerson = Plus.PeopleApi.getCurrentPerson(mGoogleApiClient); if (currentPerson.hasImage()) { // Set the URL of the image that should be loaded into this view, and // specify the ImageLoader that will be used to make the request. ((NetworkImageView) getView().findViewById(R.id.avatar)).setImageUrl(currentPerson.getImage().getUrl(), mImageLoader); } if (currentPerson.hasDisplayName()) { ((TextView) getView().findViewById(R.id.display_name)) .setText(currentPerson.getDisplayName()); } } } @Override public void onResume() { super.onResume(); mGoogleApiClient.connect(); } @Override public void onPause() { super.onPause(); mGoogleApiClient.disconnect(); } @Override public void onConnected(Bundle bundle) { if (mGridView.getAdapter() != null) { ((UploadedVideoAdapter) mGridView.getAdapter()).notifyDataSetChanged(); } setProfileInfo(); mCallbacks.onConnected(Plus.AccountApi.getAccountName(mGoogleApiClient)); } @Override public void onConnectionSuspended(int i) { } @Override public void onConnectionFailed(ConnectionResult connectionResult) { if (connectionResult.hasResolution()) { Toast.makeText(getActivity(), R.string.connection_to_google_play_failed, Toast.LENGTH_SHORT) .show(); Log.e(TAG, String.format( "Connection to Play Services Failed, error: %d, reason: %s", connectionResult.getErrorCode(), connectionResult.toString())); try { connectionResult.startResolutionForResult(getActivity(), 0); } catch (IntentSender.SendIntentException e) { Log.e(TAG, e.toString(), e); } } } @Override public void onAttach(Activity activity) { super.onAttach(activity); if (!(activity instanceof Callbacks)) { throw new ClassCastException("Activity must implement callbacks."); } mCallbacks = (Callbacks) activity; mImageLoader = mCallbacks.onGetImageLoader(); } @Override public void onDetach() { super.onDetach(); mCallbacks = null; mImageLoader = null; } public interface Callbacks { public ImageLoader onGetImageLoader(); public void onVideoSelected(VideoData video); public void onConnected(String connectedAccountName); } private class UploadedVideoAdapter extends BaseAdapter { private List mVideos; private UploadedVideoAdapter(List videos) { mVideos = videos; } @Override public int getCount() { return mVideos.size(); } @Override public Object getItem(int i) { return mVideos.get(i); } @Override public long getItemId(int i) { return mVideos.get(i).getYouTubeId().hashCode(); } @Override public View getView(final int position, View convertView, ViewGroup container) { if (convertView == null) { convertView = LayoutInflater.from(getActivity()).inflate( R.layout.list_item, container, false); } VideoData video = mVideos.get(position); ((TextView) convertView.findViewById(android.R.id.text1)) .setText(video.getTitle()); ((NetworkImageView) convertView.findViewById(R.id.thumbnail)).setImageUrl(video.getThumbUri(), mImageLoader); if (mGoogleApiClient.isConnected()) { ((PlusOneButton) convertView.findViewById(R.id.plus_button)) .initialize(video.getWatchUri(), null); } convertView.findViewById(R.id.main_target).setOnClickListener( new View.OnClickListener() { @Override public void onClick(View view) { mCallbacks.onVideoSelected(mVideos.get(position)); } }); return convertView; } } } ================================================ FILE: app/src/main/java/com/google/ytdl/util/LruBitmapCache.java ================================================ /* * Copyright (c) 2015 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.ytdl.util; import android.content.Context; import android.graphics.Bitmap; import android.support.v4.util.LruCache; import android.util.DisplayMetrics; import com.android.volley.toolbox.ImageLoader.ImageCache; public class LruBitmapCache extends LruCache implements ImageCache { public LruBitmapCache(int maxSize) { super(maxSize); } public LruBitmapCache(Context ctx) { this(getCacheSize(ctx)); } // Returns a cache size equal to approximately three screens worth of images. public static int getCacheSize(Context ctx) { final DisplayMetrics displayMetrics = ctx.getResources(). getDisplayMetrics(); final int screenWidth = displayMetrics.widthPixels; final int screenHeight = displayMetrics.heightPixels; // 4 bytes per pixel final int screenBytes = screenWidth * screenHeight * 4; return screenBytes * 3; } @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes() * value.getHeight(); } @Override public Bitmap getBitmap(String url) { return get(url); } @Override public void putBitmap(String url, Bitmap bitmap) { put(url, bitmap); } } ================================================ FILE: app/src/main/java/com/google/ytdl/util/NetworkSingleton.java ================================================ /* * Copyright (c) 2015 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.ytdl.util; import android.content.Context; import com.android.volley.Request; import com.android.volley.RequestQueue; import com.android.volley.toolbox.ImageLoader; import com.android.volley.toolbox.Volley; public class NetworkSingleton { private static NetworkSingleton mInstance; private static Context mCtx; private RequestQueue mRequestQueue; private ImageLoader mImageLoader; private NetworkSingleton(Context context) { mCtx = context; mRequestQueue = getRequestQueue(); mImageLoader = new ImageLoader(mRequestQueue, new LruBitmapCache(context)); } public static synchronized NetworkSingleton getInstance(Context context) { if (mInstance == null) { mInstance = new NetworkSingleton(context); } return mInstance; } public RequestQueue getRequestQueue() { if (mRequestQueue == null) { // getApplicationContext() is key, it keeps you from leaking the // Activity or BroadcastReceiver if someone passes one in. mRequestQueue = Volley.newRequestQueue(mCtx.getApplicationContext()); } return mRequestQueue; } public void addToRequestQueue(Request req) { getRequestQueue().add(req); } public ImageLoader getImageLoader() { return mImageLoader; } } ================================================ FILE: app/src/main/java/com/google/ytdl/util/Upload.java ================================================ /* * Copyright (c) 2013 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.ytdl.util; import com.google.ytdl.Constants; public class Upload { public static String generateKeywordFromPlaylistId(String playlistId) { if (playlistId == null) playlistId = ""; if (playlistId.indexOf("PL") == 0) { playlistId = playlistId.substring(2); } playlistId = playlistId.replaceAll("\\W", ""); String keyword = Constants.DEFAULT_KEYWORD.concat(playlistId); if (keyword.length() > Constants.MAX_KEYWORD_LENGTH) { keyword = keyword.substring(0, Constants.MAX_KEYWORD_LENGTH); } return keyword; } } ================================================ FILE: app/src/main/java/com/google/ytdl/util/Utils.java ================================================ /* Copyright (c) 2013 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.ytdl.util; import android.app.Activity; import android.content.res.Resources; import android.os.Build; import android.util.Log; import android.widget.Toast; import com.google.android.gms.auth.GoogleAuthException; import com.google.api.client.googleapis.json.GoogleJsonError; import com.google.api.client.googleapis.json.GoogleJsonResponseException; import com.google.ytdl.R; /** * Class containing some static utility methods. */ public class Utils { private Utils() { } public static boolean hasFroyo() { // Can use static final constants like FROYO, declared in later versions // of the OS since they are inlined at compile time. This is guaranteed behavior. return Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO; } public static boolean hasGingerbread() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD; } public static boolean hasHoneycomb() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; } public static boolean hasHoneycombMR1() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1; } public static boolean hasJellyBean() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; } /** * Logs the given throwable and shows an error alert dialog with its message. * * @param activity activity * @param tag log tag to use * @param t throwable to log and show */ public static void logAndShow(Activity activity, String tag, Throwable t) { Log.e(tag, "Error", t); String message = t.getMessage(); if (t instanceof GoogleJsonResponseException) { GoogleJsonError details = ((GoogleJsonResponseException) t).getDetails(); if (details != null) { message = details.getMessage(); } } else if (t.getCause() instanceof GoogleAuthException) { message = ((GoogleAuthException) t.getCause()).getMessage(); } showError(activity, message); } /** * Logs the given message and shows an error alert dialog with it. * * @param activity activity * @param tag log tag to use * @param message message to log and show or {@code null} for none */ public static void logAndShowError(Activity activity, String tag, String message) { String errorMessage = getErrorMessage(activity, message); Log.e(tag, errorMessage); showErrorInternal(activity, errorMessage); } /** * Shows an error alert dialog with the given message. * * @param activity activity * @param message message to show or {@code null} for none */ public static void showError(Activity activity, String message) { String errorMessage = getErrorMessage(activity, message); showErrorInternal(activity, errorMessage); } private static void showErrorInternal(final Activity activity, final String errorMessage) { activity.runOnUiThread(new Runnable() { public void run() { Toast.makeText(activity, errorMessage, Toast.LENGTH_LONG).show(); } }); } private static String getErrorMessage(Activity activity, String message) { Resources resources = activity.getResources(); if (message == null) { return resources.getString(R.string.error); } return resources.getString(R.string.error_format, message); } } ================================================ FILE: app/src/main/java/com/google/ytdl/util/VideoData.java ================================================ /* * Copyright (c) 2013 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.ytdl.util; import com.google.api.services.youtube.model.Video; import com.google.api.services.youtube.model.VideoSnippet; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * @author Ibrahim Ulukaya *

* Helper class to handle YouTube videos. */ public class VideoData { private Video mVideo; public Video getVideo() { return mVideo; } public void setVideo(Video video) { mVideo = video; } public String getYouTubeId() { return mVideo.getId(); } public String getTitle() { return mVideo.getSnippet().getTitle(); } public VideoSnippet addTags(Collection tags) { VideoSnippet mSnippet = mVideo.getSnippet(); List mTags = mSnippet.getTags(); if (mTags == null) { mTags = new ArrayList(2); } mTags.addAll(tags); return mSnippet; } public String getThumbUri() { return mVideo.getSnippet().getThumbnails().getDefault().getUrl(); } public String getWatchUri() { return "http://www.youtube.com/watch?v=" + getYouTubeId(); } } ================================================ FILE: app/src/main/res/drawable/list_divider_horizontal_inset.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================