diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..08939fb --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,10 @@ +## Expected Behavior + +## Actual Behavior + + +## Steps to Reproduce the Problem + +1. +1. +1. diff --git a/.gitignore b/.gitignore index 145c5fb..57ee29d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,51 @@ -bin/* -gen/* -.settings/* \ No newline at end of file +# Built application files +*.apk +*.ap_ + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/dictionaries +.idea/libraries + +# Keystore files +# Uncomment the following line if you do not want to check your keystore files in. +#*.jks + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# Misc +.DS_Store diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..291895d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# How to become a contributor and submit your own code + +## Contributor License Agreements + +We'd love to accept your sample apps and patches! Before we can take them, we +have to jump a couple of legal hurdles. + +Please fill out either the individual or corporate Contributor License Agreement +(CLA). + + * 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](https://developers.google.com/open-source/cla/individual). + * If you work for a company that wants to allow you to contribute your work, + then you'll need to sign a + [corporate CLA](https://developers.google.com/open-source/cla/corporate). + +Follow either of the two links above to access the appropriate CLA and +instructions for how to sign and return it. Once we receive it, we'll be able to +accept your pull requests. + +## Contributing A Patch + +1. Submit an issue describing your proposed change to the repo in question. +1. The repo owner will respond to your issue promptly. +1. If your proposed change is accepted, and you haven't already done so, sign a Contributor License Agreement (see details above). +1. Fork the desired repo, develop and test your code changes. +1. Ensure that your code adheres to the existing style in the sample to which you are contributing. +1. Ensure that your code has an appropriate set of unit tests which all pass. +1. Submit a pull request! + +## Style + +Samples in this repository follow the [JavaScript Semi-Standard +Style](https://github.com/Flet/semistandard). diff --git a/LICENSE b/LICENSE index cc9b8e4..261eeb9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Apache License + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -178,7 +178,7 @@ Apache License 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 "{}" + 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 @@ -186,7 +186,7 @@ Apache License same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2014, Google Inc + 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. @@ -199,4 +199,3 @@ Apache License 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. - diff --git a/README.md b/README.md index c97e596..dfc759f 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,21 @@ -# Google Drive Android API Demos - -Google Drive Android API Demos app illustrates all possible ways to talk to -Drive service with the use of interfaces available in [Google Play -Services](http://developer.android.com/google/play-services). The calls -illustrated within the app are: - -### Listing and querying -* [List files with pagination](https://github.com/googledrive/android-demos/blob/master/src/com/google/android/gms/drive/sample/demo/ListFilesActivity.java) -* [Query files](https://github.com/googledrive/android-demos/blob/master/src/com/google/android/gms/drive/sample/demo/QueryFilesActivity.java) - -### Working with files and folders -* [Create a file](https://github.com/googledrive/android-demos/blob/master/src/com/google/android/gms/drive/sample/demo/CreateFileActivity.java) -* [Create a file in App Folder](https://github.com/googledrive/android-demos/blob/master/src/com/google/android/gms/drive/sample/demo/CreateFileInAppFolderActivity.java) -* [Create a folder](https://github.com/googledrive/android-demos/blob/master/src/com/google/android/gms/drive/sample/demo/CreateFolderActivity.java) -* [Retrieve metadata](https://github.com/googledrive/android-demos/blob/master/src/com/google/android/gms/drive/sample/demo/RetrieveMetadataActivity.java) -* [Retrieve contents](https://github.com/googledrive/android-demos/blob/master/src/com/google/android/gms/drive/sample/demo/RetrieveContentsActivity.java) -* [Listen download progress](https://github.com/googledrive/android-demos/blob/master/src/com/google/android/gms/drive/sample/demo/RetrieveContentsWithProgressDialogActivity.java) -* [Edit metadata](https://github.com/googledrive/android-demos/blob/master/src/com/google/android/gms/drive/sample/demo/EditMetadataActivity.java) -* [Edit contents](https://github.com/googledrive/android-demos/blob/master/src/com/google/android/gms/drive/sample/demo/EditContentsActivity.java) -* [Pin file to the device](https://github.com/googledrive/android-demos/blob/master/src/com/google/android/gms/drive/sample/demo/PinFileActivity.java) - -### Intents -* [Create a file with creator activity](https://github.com/googledrive/android-demos/blob/master/src/com/google/android/gms/drive/sample/demo/CreateFileWithCreatorActivity.java) -* [Pick a file with opener activity](https://github.com/googledrive/android-demos/blob/master/src/com/google/android/gms/drive/sample/demo/PickFileWithOpenerActivity.java) -* [Pick a folder with opener activity](https://github.com/googledrive/android-demos/blob/master/src/com/google/android/gms/drive/sample/demo/PickFolderWithOpenerActivity.java) - -### Hierarchical operations -* [Create a file in a folder](https://github.com/googledrive/android-demos/blob/master/src/com/google/android/gms/drive/sample/demo/CreateFileInFolderActivity.java) -* [Create a folder in a folder](https://github.com/googledrive/android-demos/blob/master/src/com/google/android/gms/drive/sample/demo/CreateFolderInFolderActivity.java) -* [List files in a folder](https://github.com/googledrive/android-demos/blob/master/src/com/google/android/gms/drive/sample/demo/ListFilesInFolderActivity.java) -* [Query files in a folder](https://github.com/googledrive/android-demos/blob/master/src/com/google/android/gms/drive/sample/demo/QueryFilesInFolderActivity.java) - -### Others -* [Authorization, authentication and client connection](https://github.com/googledrive/android-demos/blob/master/src/com/google/android/gms/drive/sample/demo/BaseDemoActivity.java) -* [Synchronous requests](https://github.com/googledrive/android-demos/blob/master/src/com/google/android/gms/drive/sample/demo/SyncRequestsActivity.java) -* [Listen for metadata and contents change events](https://github.com/googledrive/android-demos/blob/master/src/com/google/android/gms/drive/sample/demo/events/ListenChangeEventsForFilesActivity.java) - -## Can I run this app? - -If you actually want to run this sample app (though it is mostly provided so you -can read the code), you will need to register an OAuth 2.0 client for the -package `com.google.android.gms.drive.sample.demo` with your own debug keys -and set any resource IDs to those that you have access to. Resource ID definitions -are on: - -* com.google.android.gms.drive.sample.demo.BaseDemoActivity.EXISTING_FOLDER_ID -* com.google.android.gms.drive.sample.demo.BaseDemoActivity.EXISTING_FILE_ID - -![Analytics](https://ga-beacon.appspot.com/UA-46884138-1/android-demos?pixel) +# Google Workspace Android Samples [![Build Status](https://travis-ci.org/googleworkspace/android-samples.svg?branch=master)](https://travis-ci.org/googleworkspace/android-samples) +A collection of samples that demonstrate how to call Google Workspace APIs from Android. + +## Note: Deprecated/archived + +This repository is no longer actively maintained and the sample code contained here is likely outdated. + +## Products + +### Drive + +#### [Deprecation](drive/deprecation) + +A sample app demonstrating migrating from the Android API to the REST API. See +the [migration guide](https://developers.google.com/drive/android/deprecation) +for details. + +## Contributing + +Contributions welcome! See the [Contributing Guide](CONTRIBUTING.md). diff --git a/appsScript/execute/app/src/main/java/com/google/appScript/execute/Main.java b/appsScript/execute/app/src/main/java/com/google/appScript/execute/Main.java new file mode 100644 index 0000000..9409fb5 --- /dev/null +++ b/appsScript/execute/app/src/main/java/com/google/appScript/execute/Main.java @@ -0,0 +1,100 @@ +/** + * Copyright 2018 Google LLC + * + * 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. + */ +// [START apps_script_api_execute] +/** + * Call the API to run an Apps Script function that returns a list + * of folders within the user's root directory on Drive. + * + * @return list of String folder names and their IDs + * @throws IOException + */ +private List getDataFromApi() + throws IOException, GoogleAuthException { + // ID of the script to call. Acquire this from the Apps Script editor, + // under Publish > Deploy as API executable. + String scriptId = "ENTER_YOUR_SCRIPT_ID_HERE"; + + List folderList = new ArrayList(); + + // Create an execution request object. + ExecutionRequest request = new ExecutionRequest() + .setFunction("getFoldersUnderRoot"); + + // Make the request. + Operation op = + mService.scripts().run(scriptId, request).execute(); + + // Print results of request. + if (op.getError() != null) { + throw new IOException(getScriptError(op)); + } + if (op.getResponse() != null && + op.getResponse().get("result") != null) { + // The result provided by the API needs to be cast into + // the correct type, based upon what types the Apps Script + // function returns. Here, the function returns an Apps + // Script Object with String keys and values, so must be + // cast into a Java Map (folderSet). + Map folderSet = + (Map)(op.getResponse().get("result")); + + for (String id: folderSet.keySet()) { + folderList.add( + String.format("%s (%s)", folderSet.get(id), id)); + } + } + + return folderList; +} + +/** + * Interpret an error response returned by the API and return a String + * summary. + * + * @param op the Operation returning an error response + * @return summary of error response, or null if Operation returned no + * error + */ +private String getScriptError(Operation op) { + if (op.getError() == null) { + return null; + } + + // Extract the first (and only) set of error details and cast as a Map. + // The values of this map are the script's 'errorMessage' and + // 'errorType', and an array of stack trace elements (which also need to + // be cast as Maps). + Map detail = op.getError().getDetails().get(0); + List> stacktrace = + (List>)detail.get("scriptStackTraceElements"); + + java.lang.StringBuilder sb = + new StringBuilder("\nScript error message: "); + sb.append(detail.get("errorMessage")); + + if (stacktrace != null) { + // There may not be a stacktrace if the script didn't start + // executing. + sb.append("\nScript error stacktrace:"); + for (Map elem : stacktrace) { + sb.append("\n "); + sb.append(elem.get("function")); + sb.append(":"); + sb.append(elem.get("lineNumber")); + } + } + sb.append("\n"); + return sb.toString(); +} +// [END apps_script_api_execute] diff --git a/drive/conflict/.gitignore b/drive/conflict/.gitignore new file mode 100644 index 0000000..1f5d1ca --- /dev/null +++ b/drive/conflict/.gitignore @@ -0,0 +1,11 @@ +# Android Studio excludes +.idea/ +*.iml + +# Gradle build excludes +.gradle/ +build/ +local.properties + +# Misc +.DS_Store \ No newline at end of file diff --git a/drive/conflict/README.md b/drive/conflict/README.md new file mode 100644 index 0000000..30ce5cd --- /dev/null +++ b/drive/conflict/README.md @@ -0,0 +1,39 @@ +# [DEPRECATED] Google Drive Android API Conflict Sample + +The Drive Android API used in this sample is now deprecated. Please see the +[migration guide](https://developers.google.com/drive/android/deprecation) +for more information. + +--- + +This application demonstrates how to resolve remote [conflicts](https://developers.google.com/drive/android/completion#conflict) using +the [Google Android Drive API](https://developers.google.com/drive/android/intro). Keep in mind the default behavior is to +overwrite the server content with the local content when a conflict arises. Please read the +[conflict strategy documentation](https://developers.google.com/android/reference/com/google/android/gms/drive/ExecutionOptions.Builder#setConflictStrategy(int)) when +choosing to override this capability. This application uses the [CONFLICT_STRATEGY_KEEP_REMOTE](https://developers.google.com/android/reference/com/google/android/gms/drive/ExecutionOptions#CONFLICT_STRATEGY_KEEP_REMOTE) +option to allow the remote app (not the server) to manage the conflict. + +## Set Up +1. Install the [Android SDK](https://developer.android.com/sdk/index.html). +1. Download and configure the +[Google Play services SDK](https://developer.android.com/google/play-services/setup.html), +which includes the Google Drive Android API. +1. Create [Google API Console](https://console.developers.google.com/projectselector/apis/dashboard) +project and/or enable the Drive API for an existing project. +1. Register an OAuth 2.0 client for the package 'com.google.android.gms.drive.sample.conflict' +with your own [debug keys](https://developers.google.com/drive/android/auth). +See full instructions in the [Getting Started guide](https://developers.google.com/drive/android/get-started). +1. Build and install the app on 2 devices or emulators. If using Android Studio, install the app +individually on each emulator then use the AVD Manager to run both emulators simultaneously. Be +sure to select Android images that have the Play Store enabled. + +## Run the Demo +Launch the application on both devices. Both apps should like this with no items listed. +![Home Screen](images/conflict_home.png) + +Add an item to the first device's list and hit 'Update List'. +![Item Screen](images/conflict_first_item.png) + +The second device should then show the item. Now add items on the second device's list. +After updating the list, both devices should show the merge lists. +![Resolved Screen](images/conflict_resolved.png) diff --git a/drive/conflict/app/.gitignore b/drive/conflict/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/drive/conflict/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/drive/conflict/app/build.gradle b/drive/conflict/app/build.gradle new file mode 100644 index 0000000..6134761 --- /dev/null +++ b/drive/conflict/app/build.gradle @@ -0,0 +1,30 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 26 + buildToolsVersion "26.0.2" + + defaultConfig { + applicationId "com.google.android.gms.drive.sample.conflict" + minSdkVersion 21 + targetSdkVersion 26 + versionCode 2 + versionName "1.1" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + compile 'com.android.support:support-v4:26.1.0' + compile 'com.google.android.gms:play-services-auth:11.6.0' + compile 'com.google.android.gms:play-services-drive:11.6.0' +} diff --git a/drive/conflict/app/proguard-rules.pro b/drive/conflict/app/proguard-rules.pro new file mode 100644 index 0000000..0e13397 --- /dev/null +++ b/drive/conflict/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /opt/android-studio/sdk/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 *; +#} diff --git a/drive/conflict/app/src/main/AndroidManifest.xml b/drive/conflict/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0b0edae --- /dev/null +++ b/drive/conflict/app/src/main/AndroidManifest.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/drive/conflict/app/src/main/java/com/google/android/gms/drive/sample/conflict/BaseDemoActivity.java b/drive/conflict/app/src/main/java/com/google/android/gms/drive/sample/conflict/BaseDemoActivity.java new file mode 100644 index 0000000..7946fa7 --- /dev/null +++ b/drive/conflict/app/src/main/java/com/google/android/gms/drive/sample/conflict/BaseDemoActivity.java @@ -0,0 +1,119 @@ +/* + * Copyright 2014 Google Inc. All Rights Reserved. + * + * 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.android.gms.drive.sample.conflict; + +import android.app.Activity; +import android.content.Intent; +import android.util.Log; +import android.widget.Toast; + +import com.google.android.gms.auth.api.signin.GoogleSignIn; +import com.google.android.gms.auth.api.signin.GoogleSignInAccount; +import com.google.android.gms.auth.api.signin.GoogleSignInClient; +import com.google.android.gms.auth.api.signin.GoogleSignInOptions; +import com.google.android.gms.drive.Drive; +import com.google.android.gms.drive.DriveClient; +import com.google.android.gms.drive.DriveResourceClient; + +/** + * An abstract activity that handles authorization and connection to the Drive + * services. + */ +public abstract class BaseDemoActivity extends Activity { + private static final String TAG = "BaseDemoActivity"; + + /** + * Request code for auto Google Play Services error resolution. + */ + private static final int REQUEST_CODE_SIGN_IN = 0; + + /** + * Handles high-level drive functions like sync. + */ + private DriveClient mDriveClient; + + /** + * Handles access to Drive resources/files. + */ + private DriveResourceClient mDriveResourceClient; + + @Override + protected void onStart() { + super.onStart(); + signIn(); + } + + /** + * Handles resolution callbacks. + */ + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == REQUEST_CODE_SIGN_IN) { + if (resultCode != RESULT_OK) { + // Sign-in may fail or be cancelled by the user. For this sample, sign-in is + // required and is fatal. For apps where sign-in is optional, handle appropriately. + Log.e(TAG, "Sign-in failed."); + finish(); + return; + } + + // We can use last signed in account here because we know the account has Drive scopes. + initializeDriveClient(GoogleSignIn.getLastSignedInAccount(this)); + } + } + + /** + * Starts the sign-in process and initializes the Drive client. + */ + private void signIn() { + GoogleSignInOptions signInOptions = + new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestScopes(Drive.SCOPE_FILE) + .requestScopes(Drive.SCOPE_APPFOLDER) + .build(); + GoogleSignInClient googleSignInClient = GoogleSignIn.getClient(this, signInOptions); + startActivityForResult(googleSignInClient.getSignInIntent(), REQUEST_CODE_SIGN_IN); + } + + /** + * Continues the sign-in process, initializing the DriveResourceClient with the current + * user's account. + */ + private void initializeDriveClient(GoogleSignInAccount signInAccount) { + mDriveClient = Drive.getDriveClient(getApplicationContext(), signInAccount); + mDriveResourceClient = Drive.getDriveResourceClient(getApplicationContext(), signInAccount); + onDriveClientReady(); + } + + /** + * Shows a toast message. + */ + void showMessage(String message) { + Toast.makeText(this, message, Toast.LENGTH_LONG).show(); + } + + /** + * Called after the user has signed in and the Drive client has been initialized. + */ + protected abstract void onDriveClientReady(); + + DriveClient getDriveClient() { + return mDriveClient; + } + + DriveResourceClient getDriveResourceClient() { + return mDriveResourceClient; + } +} diff --git a/drive/conflict/app/src/main/java/com/google/android/gms/drive/sample/conflict/ConflictResolver.java b/drive/conflict/app/src/main/java/com/google/android/gms/drive/sample/conflict/ConflictResolver.java new file mode 100644 index 0000000..d9e51e6 --- /dev/null +++ b/drive/conflict/app/src/main/java/com/google/android/gms/drive/sample/conflict/ConflictResolver.java @@ -0,0 +1,170 @@ +/* + * Copyright 2014 Google Inc. All Rights Reserved. + * + * 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.android.gms.drive.sample.conflict; + +import android.content.Context; +import android.content.Intent; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; + +import com.google.android.gms.auth.api.signin.GoogleSignIn; +import com.google.android.gms.auth.api.signin.GoogleSignInAccount; +import com.google.android.gms.auth.api.signin.GoogleSignInClient; +import com.google.android.gms.auth.api.signin.GoogleSignInOptions; +import com.google.android.gms.drive.Drive; +import com.google.android.gms.drive.DriveContents; +import com.google.android.gms.drive.DriveFile; +import com.google.android.gms.drive.DriveId; +import com.google.android.gms.drive.DriveResourceClient; +import com.google.android.gms.drive.ExecutionOptions; +import com.google.android.gms.drive.MetadataChangeSet; +import com.google.android.gms.drive.events.CompletionEvent; +import com.google.android.gms.tasks.Continuation; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.concurrent.ExecutorService; + +/** + * ConflictResolver handles a CompletionEvent with a conflict status. + */ +class ConflictResolver { + private static final String TAG = "ConflictResolver"; + static final String CONFLICT_RESOLVED = + "com.google.android.gms.drive.sample.conflict.CONFLICT_RESOLVED"; + + private LocalBroadcastManager mBroadcaster; + private CompletionEvent mConflictedCompletionEvent; + private Context mContext; + private DriveResourceClient mDriveResourceClient; + private DriveContents mDriveContents; + private String mBaseContent; + private String mModifiedContent; + private String mServerContent; + private String mResolvedContent; + private ExecutorService mExecutorService; + + ConflictResolver(CompletionEvent conflictedCompletionEvent, Context context, + ExecutorService executorService) { + this.mConflictedCompletionEvent = conflictedCompletionEvent; + mBroadcaster = LocalBroadcastManager.getInstance(context); + mContext = context; + mExecutorService = executorService; + } + + /** + * Initiate the resolution process by connecting the GoogleApiClient. + */ + void resolve() { + // [START drive_android_resolve_conflict] + // A new DriveResourceClient should be created to handle each new CompletionEvent since each + // event is tied to a specific user account. Any DriveFile action taken must be done using + // the correct account. + GoogleSignInOptions.Builder signInOptionsBuilder = + new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestScopes(Drive.SCOPE_FILE) + .requestScopes(Drive.SCOPE_APPFOLDER); + if (mConflictedCompletionEvent.getAccountName() != null) { + signInOptionsBuilder.setAccountName(mConflictedCompletionEvent.getAccountName()); + } + GoogleSignInClient signInClient = + GoogleSignIn.getClient(mContext, signInOptionsBuilder.build()); + signInClient.silentSignIn() + .continueWith(mExecutorService, + (Continuation) signInTask -> { + mDriveResourceClient = Drive.getDriveResourceClient( + mContext, signInTask.getResult()); + mBaseContent = ConflictUtil.getStringFromInputStream( + mConflictedCompletionEvent.getBaseContentsInputStream()); + mModifiedContent = ConflictUtil.getStringFromInputStream( + mConflictedCompletionEvent + .getModifiedContentsInputStream()); + return null; + }) + .continueWithTask(mExecutorService, + task -> { + DriveId driveId = mConflictedCompletionEvent.getDriveId(); + return mDriveResourceClient.openFile( + driveId.asDriveFile(), DriveFile.MODE_READ_ONLY); + }) + .continueWithTask(mExecutorService, + task -> { + mDriveContents = task.getResult(); + InputStream serverInputStream = task.getResult().getInputStream(); + mServerContent = + ConflictUtil.getStringFromInputStream(serverInputStream); + return mDriveResourceClient.reopenContentsForWrite(mDriveContents); + }) + .continueWithTask(mExecutorService, + task -> { + DriveContents contentsForWrite = task.getResult(); + mResolvedContent = ConflictUtil.resolveConflict( + mBaseContent, mServerContent, mModifiedContent); + + OutputStream outputStream = contentsForWrite.getOutputStream(); + try (Writer writer = new OutputStreamWriter(outputStream)) { + writer.write(mResolvedContent); + } + + // It is not likely that resolving a conflict will result in another + // conflict, but it can happen if the file changed again while this + // conflict was resolved. Since we already implemented conflict + // resolution and we never want to miss user data, we commit here + // with execution options in conflict-aware mode (otherwise we would + // overwrite server content). + ExecutionOptions executionOptions = + new ExecutionOptions.Builder() + .setNotifyOnCompletion(true) + .setConflictStrategy( + ExecutionOptions + .CONFLICT_STRATEGY_KEEP_REMOTE) + .build(); + + // Commit resolved contents. + MetadataChangeSet modifiedMetadataChangeSet = + mConflictedCompletionEvent.getModifiedMetadataChangeSet(); + return mDriveResourceClient.commitContents(contentsForWrite, + modifiedMetadataChangeSet, executionOptions); + }) + .addOnSuccessListener(aVoid -> { + mConflictedCompletionEvent.dismiss(); + Log.d(TAG, "resolved list"); + sendResult(mModifiedContent); + }) + .addOnFailureListener(e -> { + // The contents cannot be reopened at this point, probably due to + // connectivity, so by snoozing the event we will get it again later. + Log.d(TAG, "Unable to write resolved content, snoozing completion event.", + e); + mConflictedCompletionEvent.snooze(); + if (mDriveContents != null) { + mDriveResourceClient.discardContents(mDriveContents); + } + }); + // [END drive_android_resolve_conflict] + } + + /** + * Notify the UI that the list should be updated. + * + * @param resolution Resolved grocery list. + */ + private void sendResult(String resolution) { + Intent intent = new Intent(CONFLICT_RESOLVED); + intent.putExtra("conflictResolution", resolution); + mBroadcaster.sendBroadcast(intent); + } +} diff --git a/drive/conflict/app/src/main/java/com/google/android/gms/drive/sample/conflict/ConflictUtil.java b/drive/conflict/app/src/main/java/com/google/android/gms/drive/sample/conflict/ConflictUtil.java new file mode 100644 index 0000000..8396b09 --- /dev/null +++ b/drive/conflict/app/src/main/java/com/google/android/gms/drive/sample/conflict/ConflictUtil.java @@ -0,0 +1,87 @@ +/* + * Copyright 2014 Google Inc. All Rights Reserved. + * + * 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.android.gms.drive.sample.conflict; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +class ConflictUtil { + /** + * Performs a three-way merge of three sets of "grocery items" into one set. + * + * @param baseStr Items local modifications are based on. + * @param currentStr Items currently on the server. + * @param modifiedStr Locally modified items. + * @return Items merged from three sets of items provided. + */ + static String resolveConflict(String baseStr, String currentStr, String modifiedStr) { + List baseItems = Arrays.asList(baseStr.split("\n")); + List currentItems = Arrays.asList(currentStr.split("\n")); + List modifiedItems = Arrays.asList(modifiedStr.split("\n")); + List allItems = new ArrayList<>(); + + // Add unique items to allItems. + allItems.addAll(baseItems); + for (String item : currentItems) { + if (!allItems.contains(item)) { + allItems.add(item); + } + } + for (String item : modifiedItems) { + if (!allItems.contains(item)) { + allItems.add(item); + } + } + + // Remove items that were removed from currentItems or modifiedItems. + for (Iterator iter = allItems.iterator(); iter.hasNext();) { + String item = iter.next(); + if (baseItems.contains(item) + && (!currentItems.contains(item) || !modifiedItems.contains(item))) { + iter.remove(); + } + } + StringBuilder stringBuilder = new StringBuilder(); + for (String item : allItems) { + stringBuilder.append(item); + stringBuilder.append("\n"); + } + return stringBuilder.toString(); + } + + /** + * Gets String from InputStream. + * + * @param is InputStream used to read into String. + * @return String resulting from reading is. + */ + static String getStringFromInputStream(InputStream is) { + StringBuilder sb = new StringBuilder(); + String line; + try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) { + while ((line = br.readLine()) != null) { + sb.append(line).append("\n"); + } + } catch (IOException e) { + throw new RuntimeException("Unable to read string content."); + } + return sb.toString(); + } +} diff --git a/drive/conflict/app/src/main/java/com/google/android/gms/drive/sample/conflict/MainActivity.java b/drive/conflict/app/src/main/java/com/google/android/gms/drive/sample/conflict/MainActivity.java new file mode 100644 index 0000000..7e045c5 --- /dev/null +++ b/drive/conflict/app/src/main/java/com/google/android/gms/drive/sample/conflict/MainActivity.java @@ -0,0 +1,215 @@ +/* + * Copyright 2014 Google Inc. All Rights Reserved. + * + * 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.android.gms.drive.sample.conflict; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; +import android.widget.Button; +import android.widget.EditText; + +import com.google.android.gms.drive.DriveContents; +import com.google.android.gms.drive.DriveFile; +import com.google.android.gms.drive.DriveFolder; +import com.google.android.gms.drive.DriveId; +import com.google.android.gms.drive.ExecutionOptions; +import com.google.android.gms.drive.MetadataBuffer; +import com.google.android.gms.drive.MetadataChangeSet; +import com.google.android.gms.drive.query.Filters; +import com.google.android.gms.drive.query.Query; +import com.google.android.gms.drive.query.SearchableField; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; + +/** + * Main Activity of the application where "Grocery List" is displayed, edited and saved. + */ +public class MainActivity extends BaseDemoActivity { + private static final String TAG = "MainActivity"; + + private EditText mEditText; + private Button mUpdateGroceryListButton; + // Instance variables used for DriveFile and DriveContents to help initiate file conflicts. + private DriveFile mGroceryListFile; + private DriveContents mDriveContents; + // Receiver used to update the EditText once conflicts have been resolved. + private BroadcastReceiver mBroadcastReceiver; + + @Override + protected void onCreate(Bundle b) { + super.onCreate(b); + setContentView(R.layout.activity_main); + + mEditText = findViewById(R.id.editText); + mUpdateGroceryListButton = findViewById(R.id.button); + mUpdateGroceryListButton.setOnClickListener(view -> { + if (mGroceryListFile != null) { + mUpdateGroceryListButton.setEnabled(false); + mEditText.setEnabled(false); + saveFile() + .addOnCompleteListener(task -> { + mEditText.setEnabled(true); + mUpdateGroceryListButton.setEnabled(true); + }) + .addOnFailureListener(e -> { + Log.e(TAG, "Unexpected error", e); + showMessage(getString(R.string.unexpected_error)); + }); + } + }); + + // When conflicts are resolved, update the EditText with the resolved list + // then open the contents so it contains the resolved list. + mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(ConflictResolver.CONFLICT_RESOLVED)) { + Log.d(TAG, "Received intent to update edit text."); + showMessage(getString(R.string.reload_after_conflict)); + loadContents(mGroceryListFile).addOnFailureListener(e -> { + Log.e(TAG, "Unexpected error", e); + showMessage(getString(R.string.unexpected_error)); + }); + } + } + }; + } + + @Override + protected void onStart() { + super.onStart(); + LocalBroadcastManager.getInstance(this).registerReceiver( + mBroadcastReceiver, new IntentFilter(ConflictResolver.CONFLICT_RESOLVED)); + } + + @Override + protected void onStop() { + LocalBroadcastManager.getInstance(this).unregisterReceiver(mBroadcastReceiver); + super.onStop(); + } + + @Override + protected void onDriveClientReady() { + getDriveClient() + .requestSync() + .continueWithTask(task -> initializeGroceryList()) + .addOnFailureListener(e -> { + Log.e(TAG, "Unexpected error", e); + showMessage(getString(R.string.unexpected_error)); + }); + } + + /** + * Retrieves the list from Drive if it exists. If not, create a new list. + */ + private Task initializeGroceryList() { + Log.d(TAG, "Locating grocery list file."); + Query query = new Query.Builder() + .addFilter(Filters.eq(SearchableField.TITLE, + getResources().getString(R.string.groceryListFileName))) + .build(); + return getDriveResourceClient() + .query(query) + .continueWithTask(task -> { + MetadataBuffer metadataBuffer = task.getResult(); + try { + if (metadataBuffer.getCount() == 0) { + return createNewFile(); + } else { + DriveId id = metadataBuffer.get(0).getDriveId(); + return Tasks.forResult(id.asDriveFile()); + } + } finally { + metadataBuffer.release(); + } + }) + .continueWithTask(task -> loadContents(task.getResult())); + } + + /** + * Gets the grocery list items. + */ + private Task loadContents(DriveFile file) { + mGroceryListFile = file; + Task loadTask = + getDriveResourceClient().openFile(file, DriveFile.MODE_READ_ONLY); + return loadTask.continueWith(task -> { + Log.d(TAG, "Reading file contents."); + mDriveContents = task.getResult(); + InputStream inputStream = mDriveContents.getInputStream(); + String groceryListStr = ConflictUtil.getStringFromInputStream(inputStream); + + mEditText.setText(groceryListStr); + return null; + }); + } + + private Task createNewFile() { + Log.d(TAG, "Creating new grocery list."); + return getDriveResourceClient().getRootFolder().continueWithTask( + task -> { + DriveFolder folder = task.getResult(); + MetadataChangeSet changeSet = new MetadataChangeSet.Builder() + .setTitle(getResources().getString( + R.string.groceryListFileName)) + .setMimeType("text/plain") + .build(); + + return getDriveResourceClient().createFile(folder, changeSet, null); + }); + } + + private Task saveFile() { + Log.d(TAG, "Saving file."); + // [START drive_android_reopen_for_write] + Task reopenTask = + getDriveResourceClient().reopenContentsForWrite(mDriveContents); + // [END drive_android_reopen_for_write] + return reopenTask + .continueWithTask(task -> { + // [START drive_android_write_conflict_strategy] + DriveContents driveContents = task.getResult(); + OutputStream outputStream = driveContents.getOutputStream(); + try (Writer writer = new OutputStreamWriter(outputStream)) { + writer.write(mEditText.getText().toString()); + } + // ExecutionOptions define the conflict strategy to be used. + // [START drive_android_execution_options] + ExecutionOptions executionOptions = + new ExecutionOptions.Builder() + .setNotifyOnCompletion(true) + .setConflictStrategy( + ExecutionOptions.CONFLICT_STRATEGY_KEEP_REMOTE) + .build(); + return getDriveResourceClient().commitContents( + driveContents, null, executionOptions); + // [END drive_android_execution_options] + // [END drive_android_write_conflict_strategy] + }) + .continueWithTask(task -> { + showMessage(getString(R.string.file_saved)); + Log.d(TAG, "Reopening file for read."); + return loadContents(mGroceryListFile); + }); + } +} diff --git a/drive/conflict/app/src/main/java/com/google/android/gms/drive/sample/conflict/MyDriveEventService.java b/drive/conflict/app/src/main/java/com/google/android/gms/drive/sample/conflict/MyDriveEventService.java new file mode 100644 index 0000000..265b898 --- /dev/null +++ b/drive/conflict/app/src/main/java/com/google/android/gms/drive/sample/conflict/MyDriveEventService.java @@ -0,0 +1,84 @@ +/* + * Copyright 2014 Google Inc. All Rights Reserved. + * + * 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.android.gms.drive.sample.conflict; + +import android.util.Log; + +import com.google.android.gms.drive.events.CompletionEvent; +import com.google.android.gms.drive.events.DriveEventService; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +// [START drive_android_on_completion] +public class MyDriveEventService extends DriveEventService { + private static final String TAG = "MyDriveEventService"; + private ExecutorService mExecutorService; + + @Override + public void onCreate() { + super.onCreate(); + // [START_EXCLUDE] + mExecutorService = Executors.newSingleThreadExecutor(); + // [END_EXCLUDE] + } + + @Override + public synchronized void onDestroy() { + super.onDestroy(); + // [START_EXCLUDE] + mExecutorService.shutdown(); + // [END_EXCLUDE] + } + + @Override + public void onCompletion(CompletionEvent event) { + boolean eventHandled = false; + switch (event.getStatus()) { + case CompletionEvent.STATUS_SUCCESS: + // Commit completed successfully. + // Can now access the remote resource Id + // [START_EXCLUDE] + String resourceId = event.getDriveId().getResourceId(); + Log.d(TAG, "Remote resource ID: " + resourceId); + eventHandled = true; + // [END_EXCLUDE] + break; + case CompletionEvent.STATUS_FAILURE: + // Handle failure.... + // Modified contents and metadata failed to be applied to the server. + // They can be retrieved from the CompletionEvent to try to be applied later. + // [START_EXCLUDE] + // CompletionEvent is only dismissed here. In a real world application failure + // should be handled before the event is dismissed. + eventHandled = true; + // [END_EXCLUDE] + break; + case CompletionEvent.STATUS_CONFLICT: + // Handle completion conflict. + // [START_EXCLUDE] + ConflictResolver conflictResolver = + new ConflictResolver(event, this, mExecutorService); + conflictResolver.resolve(); + eventHandled = false; // Resolver will snooze or dismiss + // [END_EXCLUDE] + break; + } + + if (eventHandled) { + event.dismiss(); + } + } +} +// [END drive_android_on_completion] \ No newline at end of file diff --git a/drive/conflict/app/src/main/res/drawable-hdpi/ic_launcher.png b/drive/conflict/app/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..266c7e3 Binary files /dev/null and b/drive/conflict/app/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/drive/conflict/app/src/main/res/drawable-mdpi/ic_launcher.png b/drive/conflict/app/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000..ac508b5 Binary files /dev/null and b/drive/conflict/app/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/drive/conflict/app/src/main/res/drawable-xhdpi/ic_launcher.png b/drive/conflict/app/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000..d325ef7 Binary files /dev/null and b/drive/conflict/app/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/drive/conflict/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/drive/conflict/app/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..dced355 Binary files /dev/null and b/drive/conflict/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/drive/conflict/app/src/main/res/layout/activity_main.xml b/drive/conflict/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..cb31483 --- /dev/null +++ b/drive/conflict/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,51 @@ + + + + + + + +