I am trying to access the google drive through android app. I have turned on the Drive API and Drive SDK in Google Developer Console and generated a OAuth Client id.
Inserted the Client key in AndroidManifest.xml as
<meta-data
android:name="com.google.android.apps.drive.APP_ID"
android:value=id="***CLIENT_KEY***" />
And a permission as
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
<uses-permission android:name="android.permission.INTERNET"/>
This is the code which I am trying to run (Originally from here)
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import android.app.Activity;
import android.content.Intent;
import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
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.common.api.ResultCallback;
import com.google.android.gms.drive.Drive;
import com.google.android.gms.drive.DriveApi.DriveContentsResult;
import com.google.android.gms.drive.MetadataChangeSet;
/**
* Android Drive Quickstart activity. This activity takes a photo and saves it
* in Google Drive. The user is prompted with a pre-made dialog which allows
* them to choose the file location.
*/
public class MainActivity extends Activity implements ConnectionCallbacks,
OnConnectionFailedListener {
private static final String TAG = "android-drive-quickstart";
private static final int REQUEST_CODE_CAPTURE_IMAGE = 1;
private static final int REQUEST_CODE_CREATOR = 2;
private static final int REQUEST_CODE_RESOLUTION = 3;
private GoogleApiClient mGoogleApiClient;
private Bitmap mBitmapToSave;
/**
* Create a new file and save it to Drive.
*/
private void saveFileToDrive() {
// Start by creating a new contents, and setting a callback.
Log.i(TAG, "Creating new contents.");
final Bitmap image = mBitmapToSave;
Drive.DriveApi.newDriveContents(mGoogleApiClient)
.setResultCallback(new ResultCallback<DriveContentsResult>() {
#Override
public void onResult(DriveContentsResult result) {
// If the operation was not successful, we cannot do anything
// and must
// fail.
if (!result.getStatus().isSuccess()) {
Log.i(TAG, "Failed to create new contents.");
return;
}
// Otherwise, we can write our data to the new contents.
Log.i(TAG, "New contents created.");
// Get an output stream for the contents.
OutputStream outputStream = result.getDriveContents().getOutputStream();
// Write the bitmap data from it.
ByteArrayOutputStream bitmapStream = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.PNG, 100, bitmapStream);
try {
outputStream.write(bitmapStream.toByteArray());
} catch (IOException e1) {
Log.i(TAG, "Unable to write file contents.");
}
// Create the initial metadata - MIME type and title.
// Note that the user will be able to change the title later.
MetadataChangeSet metadataChangeSet = new MetadataChangeSet.Builder()
.setMimeType("image/jpeg").setTitle("Android Photo.png").build();
// Create an intent for the file chooser, and start it.
IntentSender intentSender = Drive.DriveApi
.newCreateFileActivityBuilder()
.setInitialMetadata(metadataChangeSet)
.setInitialDriveContents(result.getDriveContents())
.build(mGoogleApiClient);
try {
startIntentSenderForResult(
intentSender, REQUEST_CODE_CREATOR, null, 0, 0, 0);
} catch (SendIntentException e) {
Log.i(TAG, "Failed to launch file chooser.");
}
}
});
}
#Override
protected void onResume() {
super.onResume();
if (mGoogleApiClient == null) {
// Create the API client and bind it to an instance variable.
// We use this instance as the callback for connection and connection
// failures.
// Since no account name is passed, the user is prompted to choose.
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(Drive.API)
.addScope(Drive.SCOPE_FILE)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
}
// Connect the client. Once connected, the camera is launched.
mGoogleApiClient.connect();
}
#Override
protected void onPause() {
if (mGoogleApiClient != null) {
mGoogleApiClient.disconnect();
}
super.onPause();
}
#Override
protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
switch (requestCode) {
case REQUEST_CODE_CAPTURE_IMAGE:
// Called after a photo has been taken.
if (resultCode == Activity.RESULT_OK) {
// Store the image data as a bitmap for writing later.
mBitmapToSave = (Bitmap) data.getExtras().get("data");
}
break;
case REQUEST_CODE_CREATOR:
// Called after a file is saved to Drive.
if (resultCode == RESULT_OK) {
Log.i(TAG, "Image successfully saved.");
mBitmapToSave = null;
// Just start the camera again for another photo.
startActivityForResult(new Intent(MediaStore.ACTION_IMAGE_CAPTURE),
REQUEST_CODE_CAPTURE_IMAGE);
}
break;
}
}
#Override
public void onConnectionFailed(ConnectionResult result) {
// Called whenever the API client fails to connect.
Log.i(TAG, "GoogleApiClient connection failed: " + result.toString());
if (!result.hasResolution()) {
// show the localized error dialog.
GooglePlayServicesUtil.getErrorDialog(result.getErrorCode(), this, 0).show();
return;
}
// The failure has a resolution. Resolve it.
// Called typically when the app is not yet authorized, and an
// authorization
// dialog is displayed to the user.
try {
result.startResolutionForResult(this, REQUEST_CODE_RESOLUTION);
} catch (SendIntentException e) {
Log.e(TAG, "Exception while starting resolution activity", e);
}
}
#Override
public void onConnected(Bundle connectionHint) {
Log.i(TAG, "API client connected.");
if (mBitmapToSave == null) {
// This activity has no UI of its own. Just start the camera.
startActivityForResult(new Intent(MediaStore.ACTION_IMAGE_CAPTURE),
REQUEST_CODE_CAPTURE_IMAGE);
return;
}
saveFileToDrive();
}
#Override
public void onConnectionSuspended(int cause) {
Log.i(TAG, "GoogleApiClient connection suspended");
}
}
This is the error I am getting
02-19 18:58:18.204 27221-27221/com.gajendraprofile.drive I/android-drive-quickstart﹕ GoogleApiClient connection failed: ConnectionResult{statusCode=INTERNAL_ERROR, resolution=null}
02-19 18:58:47.584 27431-27431/com.gajendraprofile.drive I/android-drive-quickstart﹕ GoogleApiClient connection failed: ConnectionResult{statusCode=SIGN_IN_REQUIRED, resolution=PendingIntent{21b27910: android.os.BinderProxy#21b00a7c}}
02-19 18:58:51.564 27431-27431/com.gajendraprofile.drive I/android-drive-quickstart﹕ GoogleApiClient connection failed: ConnectionResult{statusCode=INTERNAL_ERROR, resolution=null}`
Am I making any errors above? Are there any better simple example to access Google Drive from Android?
The Quick Start you play with as as simple as it gets, to answer you question.
But it may be outdated (I don't know, last time I ran it was 8 months ago). GooPlayServices are on the 6.5.+ version and last update of that code was half a year ago. I have some code on GitHub that I can't claim is simpler, but (probably) more in line with current lib version. It is a bit broader and deals with both GDAA and REST APIs, as well as with the Google account pick process. If you use Android Studio, you should be able to make use of it.
Just a few points:
You have go through the Developers Console stuff. Basically you must have your 'package name' / SHA1 registered. I usually register both debug and release SHA1s and double check if my APKs are actually correct - see SO 28532206
Look at SO 28439129 here to get some sense what is involved in connecting to GooDrive
If you use the code I mentioned, make sure you environment is in line with dependencies in 'build.gradle' there (my SDK Manager shows GooPlaySvcs 21, which is 'com.google.android.gms:play-services:6.5.87')
Good Luck
This error "GoogleApiClient connection failed: ConnectionResult{statusCode=INTERNAL_ERROR, resolution=null} " occurs if you have not created credentials for your application.
Go to console- https://console.cloud.google.com/apis/credentials
Click on Create Credentials
Select QAuth client ID
Select Application type as Android if you are running from AndroidStudio
Add the projectname, SHA key & package name
Run the project and the application should work.
I faced issue running android-quickstart-master and the issue got resolved after following the above steps
Related
I want to use the SafetyNet Attestation API (mind that this documentation seems to be outdated since the methods it uses are deprecated). Using the latest version of Play Services (11.0.1) I came up with the following code:
SecureRandom secureRandom = new SecureRandom();
byte[] nonce = new byte[16];
secureRandom.nextBytes(nonce); // just some random bytes for testing
SafetyNet.getClient(this)
.attest(nonce, API_KEY)
.addOnCompleteListener(this, task -> {
if (task.isSuccessful()) {
SafetyNetApi.AttestationResponse result = task.getResult();
String jws = result.getJwsResult();
Log.d(TAG, "JWS: " + jws);
} else {
Exception e = task.getException();
if (e instanceof ApiException) {
Log.e(TAG, "Attestation failure: " + ((ApiException) e).getStatusMessage() + ", code: " + ((ApiException) e).getStatusCode(), e);
} else {
Log.e(TAG, "Attestation failure: " + e, e);
}
}
});
where API_KEY is the API key from the Google Developer Console. This code is called in an Activity's onCreate(...). Whatever I tried, it results in failure and the e is an instance of ApiException, but it does not provide any useful information about what went wrong since the status message is null and the status code is 8, which - according to the documentation - is an "internal error". I tried to call this with a 5 second delay but no success. The test device has API 24 and Google Play services 11.0.55.
Anyone has any idea what goes wrong and what's the solution for this?
Edit: the old SafetyNet.SafetyNetApi.attest(googleApiClient, nonce) way seems to work fine but it's deprecated so I don't want to use it.
Based from this thread, if you get error code 8 (INTERNAL_ERROR), please double check your app registration in dev console. Note that every registered Android client is uniquely identified by the (package name, Android Signing Certificate SHA-1) pair. If you have multiple package names / signing certificate for your debug and production environments, make sure to register every pair of them.
To verify:
Open the Credentials page and select your project
Make sure every pair has an Android typed OAuth 2.0 client IDs. To create a new OAuth 2.0 client ID for your Android client, select New Credentials->OAuth2 Client ID from the dropdown, select Android and input your Package name / Signing-certificate fingerprint there.
If it doesn't work, I recommend you to contact the Google Play team for help. You can reach them from this link here: https://support.google.com/googleplay#topic=3364260&contact=1.
Ensure that you are using correct WEB API KEY in your following code:
SafetyNet.getClient(this)
.attest(nonce, WEB_API_KEY)......
See following image to find WEB API KEY:
FCM Console
// Build.gradle
implementation 'com.google.firebase:firebase-core:17.2.1'
implementation 'com.google.firebase:firebase-messaging:20.1.0'
implementation 'com.android.support:support-annotations:28.0.0'
implementation 'com.google.android.gms:play-services-safetynet:17.0.0'
implementation 'com.google.android.gms:play-services-tasks:17.0.0'
import android.os.Bundle;
import android.util.Base64;
import android.util.Log;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.gms.safetynet.SafetyNetClient;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.safetynet.SafetyNet;
import com.google.android.gms.safetynet.SafetyNetApi;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.common.api.CommonStatusCodes;
import com.google.android.gms.safetynet.SafetyNet;
import com.google.android.gms.safetynet.SafetyNetApi;
import com.google.android.gms.safetynet.SafetyNetClient;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.common.api.CommonStatusCodes;
import com.google.android.gms.safetynet.SafetyNetApi.AttestationResponse;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Random;
#Override
public void onConnected(Bundle bundle) {
Log.d("My Project Name:", "Google play services connected");
runSafetyNetTest(mContext);
}
private byte[] getRequestNonce(String data) {
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
byte[] bytes = new byte[24];
mRandom.nextBytes(bytes);
try {
byteStream.write(bytes);
byteStream.write(data.getBytes());
} catch (IOException e) {
return null;
}
public void runSafetyNetTest(Context context) {
String nonceData = "734K78J56KJ745JH78LKJ9CSOC3477tj35f345j7" + System.currentTimeMillis();
byte[] nonce = getRequestNonce(nonceData);
SafetyNetClient client = SafetyNet.getClient(context);
Task<SafetyNetApi.AttestationResponse> task = client.attest(nonce, this.googleDeviceVerificationApiKey);
task.addOnSuccessListener( mSuccessListener).addOnFailureListener(mFailureListener);
}
private OnSuccessListener<SafetyNetApi.AttestationResponse> mSuccessListener =
new OnSuccessListener<SafetyNetApi.AttestationResponse>() {
#Override
public void onSuccess(SafetyNetApi.AttestationResponse attestationResponse) {
mResult = attestationResponse.getJwsResult();
// writeLog( "Success! SafetyNet result:\n" + mResult + "\n");
final String jwsResult = mResult;
final SafetyNetResponse response = parseJsonWebSignature(jwsResult);
lastResponse = response;
//only need to validate the response if it says we pass
if (!response.isCtsProfileMatch() || !response.isBasicIntegrity()) {
// This is Result........
callback.success(response.isCtsProfileMatch(), response.isBasicIntegrity());
return;
} else {
//validate payload of the response
if(true/*validateSafetyNetResponsePayload(response)*/) {
if (googleDeviceVerificationApiKey != "")
{
//if the api key is set, run the AndroidDeviceVerifier
AndroidDeviceVerifier androidDeviceVerifier = new AndroidDeviceVerifier(googleDeviceVerificationApiKey, jwsResult);
androidDeviceVerifier.verify(new AndroidDeviceVerifier.AndroidDeviceVerifierCallback() {
#Override
public void error(String errorMsg) {
callback.error(RESPONSE_ERROR_VALIDATING_SIGNATURE, "Response signature validation error: " + errorMsg);
}
#Override
public void success(boolean isValidSignature) {
if (isValidSignature) {
callback.success(response.isCtsProfileMatch(), response.isBasicIntegrity());
} else {
callback.error(RESPONSE_FAILED_SIGNATURE_VALIDATION, "Response signature invalid");
}
}
});
} else {
Log.w(TAG, "No google Device Verification ApiKey defined");
callback.error(RESPONSE_FAILED_SIGNATURE_VALIDATION_NO_API_KEY, "No Google Device Verification ApiKey defined. Marking as failed. SafetyNet CtsProfileMatch: " + response.isCtsProfileMatch());
}
} else {
callback.error(RESPONSE_VALIDATION_FAILED, "Response payload validation failed");
}
}
}
};
private OnFailureListener mFailureListener = new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
// An error occurred while communicating with the service.
mResult = null;
if (e instanceof ApiException) {
// An error with the Google Play Services API contains some additional details.
ApiException apiException = (ApiException) e;
writeLog( "Error: " +
CommonStatusCodes.getStatusCodeString(apiException.getStatusCode()) + ": " +
apiException.getStatusMessage());
} else {
// A different, unknown type of error occurred.
writeLog( "ERROR! " + e.getMessage());
}
}
};
I'm trying to implement offline DRM support for ExoPlayer 2 but I have some problems.
I found this conversation. There is some implementation for ExoPlayer 1.x and some steps how to work that implementation with ExoPlayer 2.x.
I have I problem with OfflineDRMSessionManager whitch implements DrmSessionManager. In that example is DrmSessionManager imported from ExoPlayer 1.x. If I import it from ExoPlayer 2 then I have a problems to compile it. I have a problem with #Override methods (open(), close(), ..) which are NOT in that new DrmSessionManager and there are some new methods: acquireSession(), ... .
With the latest release of ExoPlayer 2.2.0 , it provides this facility inbuilt in ExoPlayer. ExoPlayer has a helper class to download and refresh offline license keys. It should be the preferred way to do this.
OfflineLicenseHelper.java
/**
* Helper class to download, renew and release offline licenses. It utilizes {#link
* DefaultDrmSessionManager}.
*/
public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
You can access the latest code from the ExoPlayer repo
I created a sample application for Offline playback of DRM content.You can access it from here
As #TheJango explained, with the latest release of ExoPlayer 2.2.0 , it provides this facility inbuilt in ExoPlayer.
However, the OfflineLicenseHelper class was designed with some VOD use case in mind. Buy a movie, save the license (download method), download the movie, load the license in a DefaultDrmSessionManager and then setMode for playback.
Another use case could be that you want to make an online streaming system where different content is using the same license (e.g. Television) for quite some time (e.g. 24hours) more intelligent. So that it never downloads a license which it already has (Suppose your DRM system charges you per license request and there will be a lot of requests for the same license otherwise), the following approach can be used with ExoPlayer 2.2.0. It took me some time to get a working solution without modifying anything to the ExoPlayer source. I don't quite like the approach they've taken with the setMode() method which can only be called once. Previously DrmSessionManagers would work for multiple sessions (audio, video) and now they no longer work if licenses differ or come from different methods (DOWNLOAD, PLAYBACK, ...). Anyway, I introduced a new class CachingDefaultDrmSessionManager to replace the DefaultDrmSessionManager you are probably using. Internally it delegates to a DefaultDrmSessionManager.
package com.google.android.exoplayer2.drm;
import android.content.Context;
import android.content.SharedPreferences;
import java.util.concurrent.atomic.AtomicBoolean;
import android.os.Handler;
import android.os.Looper;
import android.util.Base64;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
import java.util.HashMap;
import java.util.UUID;
import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_DOWNLOAD;
import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_QUERY;
public class CachingDefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T> {
private final SharedPreferences drmkeys;
public static final String TAG="CachingDRM";
private final DefaultDrmSessionManager<T> delegateDefaultDrmSessionManager;
private final UUID uuid;
private final AtomicBoolean pending = new AtomicBoolean(false);
private byte[] schemeInitD;
public interface EventListener {
void onDrmKeysLoaded();
void onDrmSessionManagerError(Exception e);
void onDrmKeysRestored();
void onDrmKeysRemoved();
}
public CachingDefaultDrmSessionManager(Context context, UUID uuid, ExoMediaDrm<T> mediaDrm, MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters, final Handler eventHandler, final EventListener eventListener) {
this.uuid = uuid;
DefaultDrmSessionManager.EventListener eventListenerInternal = new DefaultDrmSessionManager.EventListener() {
#Override
public void onDrmKeysLoaded() {
saveDrmKeys();
pending.set(false);
if (eventListener!=null) eventListener.onDrmKeysLoaded();
}
#Override
public void onDrmSessionManagerError(Exception e) {
pending.set(false);
if (eventListener!=null) eventListener.onDrmSessionManagerError(e);
}
#Override
public void onDrmKeysRestored() {
saveDrmKeys();
pending.set(false);
if (eventListener!=null) eventListener.onDrmKeysRestored();
}
#Override
public void onDrmKeysRemoved() {
pending.set(false);
if (eventListener!=null) eventListener.onDrmKeysRemoved();
}
};
delegateDefaultDrmSessionManager = new DefaultDrmSessionManager<T>(uuid, mediaDrm, callback, optionalKeyRequestParameters, eventHandler, eventListenerInternal);
drmkeys = context.getSharedPreferences("drmkeys", Context.MODE_PRIVATE);
}
final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for ( int j = 0; j < bytes.length; j++ ) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
public void saveDrmKeys() {
byte[] offlineLicenseKeySetId = delegateDefaultDrmSessionManager.getOfflineLicenseKeySetId();
if (offlineLicenseKeySetId==null) {
Log.i(TAG,"Failed to download offline license key");
} else {
Log.i(TAG,"Storing downloaded offline license key for "+bytesToHex(schemeInitD)+": "+bytesToHex(offlineLicenseKeySetId));
storeKeySetId(schemeInitD, offlineLicenseKeySetId);
}
}
#Override
public DrmSession<T> acquireSession(Looper playbackLooper, DrmInitData drmInitData) {
if (pending.getAndSet(true)) {
return delegateDefaultDrmSessionManager.acquireSession(playbackLooper, drmInitData);
}
// First check if we already have this license in local storage and if it's still valid.
DrmInitData.SchemeData schemeData = drmInitData.get(uuid);
schemeInitD = schemeData.data;
Log.i(TAG,"Request for key for init data "+bytesToHex(schemeInitD));
if (Util.SDK_INT < 21) {
// Prior to L the Widevine CDM required data to be extracted from the PSSH atom.
byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitD, C.WIDEVINE_UUID);
if (psshData == null) {
// Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged.
} else {
schemeInitD = psshData;
}
}
byte[] cachedKeySetId=loadKeySetId(schemeInitD);
if (cachedKeySetId!=null) {
//Load successful.
Log.i(TAG,"Cached key set found "+bytesToHex(cachedKeySetId));
if (!Arrays.equals(delegateDefaultDrmSessionManager.getOfflineLicenseKeySetId(), cachedKeySetId))
{
delegateDefaultDrmSessionManager.setMode(MODE_QUERY, cachedKeySetId);
}
} else {
Log.i(TAG,"No cached key set found ");
delegateDefaultDrmSessionManager.setMode(MODE_DOWNLOAD,null);
}
DrmSession<T> tDrmSession = delegateDefaultDrmSessionManager.acquireSession(playbackLooper, drmInitData);
return tDrmSession;
}
#Override
public void releaseSession(DrmSession<T> drmSession) {
pending.set(false);
delegateDefaultDrmSessionManager.releaseSession(drmSession);
}
public void storeKeySetId(byte[] initData, byte[] keySetId) {
String encodedInitData = Base64.encodeToString(initData, Base64.NO_WRAP);
String encodedKeySetId = Base64.encodeToString(keySetId, Base64.NO_WRAP);
drmkeys.edit()
.putString(encodedInitData, encodedKeySetId)
.apply();
}
public byte[] loadKeySetId(byte[] initData) {
String encodedInitData = Base64.encodeToString(initData, Base64.NO_WRAP);
String encodedKeySetId = drmkeys.getString(encodedInitData, null);
if (encodedKeySetId == null) return null;
return Base64.decode(encodedKeySetId, 0);
}
}
Here keys are persisted as Base64 encoded strings in local storage. Because for a typical DASH stream both audio and video renderers will request a license from the DrmSessionManager, possibly at the same time, the AtomicBoolean is used. If audio and or video would use different keys, I think this approach would fail.
Also I am not yet checking for expired keys here. Have a look at OfflineLicenseHelper to see how to deal with those.
#Pepa Zapletal, proceed with below changes to play in offline.
You can also see the updated answer here.
Changes are as follows :
Changed signature of the method private void onKeyResponse(Object response) to private void onKeyResponse(Object response, boolean offline)
Rather than sending the file manifest URI send stored file path to PlayerActivity.java.
Change MediaDrm.KEY_TYPE_STREAMING to MediaDrm.KEY_TYPE_OFFLINE in getKeyRequest().
In postKeyRequest() first check whether the key is stored or not, if key found then directly call onKeyResponse(key, true).
In onKeyResponse(), call restoreKeys() rather than calling provideKeyResponse().
The rest everything is same, now your file will be playing.
Major role : Here provideKeyResponse() and restoreKeys() are native methods which acts major role in getting the key and restoring the key.
provideKeyResponse() method which will return us the main License key in byte array if and only if the keyType is MediaDrm.KEY_TYPE_OFFLINE else this method will return us the empty byte array with which we can do nothing with that array.
restoreKeys() method will expect the key which is to be restored for the current session, so feed the key which we have already stored in local to this method and it will take care of it.
Note : First you have to somehow download the license key and store it somewhere in local device securely.
In my case first im playing the file online, so exoplayer will fetch the key that key i have stored in local. From second time onwards first it will check whether the key is stored or not, if key found it will skip the License key request and will the play the file.
Replace the methods and inner classes of StreamingDrmSessionManager.java with these things.
private void postKeyRequest() {
KeyRequest keyRequest;
try {
// check is key exist in local or not, if exist no need to
// make a request License server for the key.
byte[] keyFromLocal = Util.getKeyFromLocal();
if(keyFromLocal != null) {
onKeyResponse(keyFromLocal, true);
return;
}
keyRequest = mediaDrm.getKeyRequest(sessionId, schemeData.data, schemeData.mimeType, MediaDrm.KEY_TYPE_OFFLINE, optionalKeyRequestParameters);
postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
} catch (NotProvisionedException e) {
onKeysError(e);
}
}
private void onKeyResponse(Object response, boolean offline) {
if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
// This event is stale.
return;
}
if (response instanceof Exception) {
onKeysError((Exception) response);
return;
}
try {
// if we have a key and we want to play offline then call
// 'restoreKeys()' with the key which we have already stored.
// Here 'response' is the stored key.
if(offline) {
mediaDrm.restoreKeys(sessionId, (byte[]) response);
} else {
// Don't have any key in local, so calling 'provideKeyResponse()' to
// get the main License key and store the returned key in local.
byte[] bytes = mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
Util.storeKeyInLocal(bytes);
}
state = STATE_OPENED_WITH_KEYS;
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
#Override
public void run() {
eventListener.onDrmKeysLoaded();
}
});
}
} catch (Exception e) {
onKeysError(e);
}
}
#SuppressLint("HandlerLeak")
private class PostResponseHandler extends Handler {
public PostResponseHandler(Looper looper) {
super(looper);
}
#Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_PROVISION:
onProvisionResponse(msg.obj);
break;
case MSG_KEYS:
// We don't have key in local so calling 'onKeyResponse()' with offline to 'false'.
onKeyResponse(msg.obj, false);
break;
}
}
}
According this thread on stackoverflow it should be possible to manage notification from outside main/UI thread. And it actually is. I'm creating notification in SyncAdapter to notify user that background sync started and updating upload progress and after upload is finished I'm canceling notification after some defined timeout. My problem is that notification auto cancelling is not predictable. Sometimes it auto cancels itself ok, sometimes it is visible until next sync.
Here is the whole Adapter:
package com.marianhello.bgloc.sync;
import android.accounts.Account;
import android.app.NotificationManager;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SyncResult;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.v4.app.NotificationCompat;
import com.marianhello.bgloc.Config;
import com.marianhello.bgloc.HttpPostService;
import com.marianhello.bgloc.UploadingCallback;
import com.marianhello.bgloc.data.ConfigurationDAO;
import com.marianhello.bgloc.data.DAOFactory;
import com.marianhello.logging.LoggerManager;
import org.json.JSONException;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.HashMap;
/**
* Handle the transfer of data between a server and an
* app, using the Android sync adapter framework.
*/
public class SyncAdapter extends AbstractThreadedSyncAdapter implements UploadingCallback {
private static final int NOTIFICATION_ID = 1;
ContentResolver contentResolver;
private ConfigurationDAO configDAO;
private NotificationManager notifyManager;
private BatchManager batchManager;
private org.slf4j.Logger log;
/**
* Set up the sync adapter
*/
public SyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
log = LoggerManager.getLogger(SyncAdapter.class);
/*
* If your app uses a content resolver, get an instance of it
* from the incoming Context
*/
contentResolver = context.getContentResolver();
configDAO = DAOFactory.createConfigurationDAO(context);
batchManager = new BatchManager(this.getContext());
notifyManager = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE);
}
/**
* Set up the sync adapter. This form of the
* constructor maintains compatibility with Android 3.0
* and later platform versions
*/
public SyncAdapter(
Context context,
boolean autoInitialize,
boolean allowParallelSyncs) {
super(context, autoInitialize, allowParallelSyncs);
log = LoggerManager.getLogger(SyncAdapter.class);
/*
* If your app uses a content resolver, get an instance of it
* from the incoming Context
*/
contentResolver = context.getContentResolver();
configDAO = DAOFactory.createConfigurationDAO(context);
batchManager = new BatchManager(this.getContext());
notifyManager = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE);
}
/*
* Specify the code you want to run in the sync adapter. The entire
* sync adapter runs in a background thread, so you don't have to set
* up your own background processing.
*/
#Override
public void onPerformSync(
Account account,
Bundle extras,
String authority,
ContentProviderClient provider,
SyncResult syncResult) {
Config config = null;
try {
config = configDAO.retrieveConfiguration();
} catch (JSONException e) {
log.error("Error retrieving config: {}", e.getMessage());
}
if (config == null) return;
log.debug("Sync request: {}", config.toString());
if (config.hasUrl() || config.hasSyncUrl()) {
Long batchStartMillis = System.currentTimeMillis();
File file = null;
try {
file = batchManager.createBatch(batchStartMillis);
} catch (IOException e) {
log.error("Failed to create batch: {}", e.getMessage());
}
if (file == null) {
log.info("Nothing to sync");
return;
}
log.info("Syncing batchStartMillis: {}", batchStartMillis);
String url = config.hasSyncUrl() ? config.getSyncUrl() : config.getUrl();
HashMap<String, String> httpHeaders = new HashMap<String, String>();
httpHeaders.putAll(config.getHttpHeaders());
httpHeaders.put("x-batch-id", String.valueOf(batchStartMillis));
if (uploadLocations(file, url, httpHeaders)) {
log.info("Batch sync successful");
batchManager.setBatchCompleted(batchStartMillis);
if (file.delete()) {
log.info("Batch file has been deleted: {}", file.getAbsolutePath());
} else {
log.warn("Batch file has not been deleted: {}", file.getAbsolutePath());
}
} else {
log.warn("Batch sync failed due server error");
syncResult.stats.numIoExceptions++;
}
}
}
private boolean uploadLocations(File file, String url, HashMap httpHeaders) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(getContext());
builder.setOngoing(true);
builder.setContentTitle("Syncing locations");
builder.setContentText("Sync in progress");
builder.setSmallIcon(android.R.drawable.ic_dialog_info);
notifyManager.notify(NOTIFICATION_ID, builder.build());
try {
int responseCode = HttpPostService.postJSON(url, file, httpHeaders, this);
if (responseCode == HttpURLConnection.HTTP_OK) {
builder.setContentText("Sync completed");
} else {
builder.setContentText("Sync failed due server error");
}
return responseCode == HttpURLConnection.HTTP_OK;
} catch (IOException e) {
log.warn("Error uploading locations: {}", e.getMessage());
builder.setContentText("Sync failed: " + e.getMessage());
} finally {
builder.setOngoing(false);
builder.setProgress(0, 0, false);
builder.setAutoCancel(true);
notifyManager.notify(NOTIFICATION_ID, builder.build());
Handler h = new Handler(Looper.getMainLooper());
long delayInMilliseconds = 5000;
h.postDelayed(new Runnable() {
public void run() {
notifyManager.cancel(NOTIFICATION_ID);
}
}, delayInMilliseconds);
}
return false;
}
public void uploadListener(int progress) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(getContext());
builder.setOngoing(true);
builder.setContentTitle("Syncing locations");
builder.setContentText("Sync in progress");
builder.setSmallIcon(android.R.drawable.ic_dialog_info);
builder.setProgress(100, progress, false);
notifyManager.notify(NOTIFICATION_ID, builder.build());
}
}
The whole project is OSS so full source code is available. To get bigger picture also HttpPostService.java might interesting.
I think your issue is the following: you post notification cancel on UI thread, but in parallel you post updates on background thread. There is race condition between cancellation and the last update(s) - sometimes cancellation is the last command that notification manager gets, and sometimes it receives additional update(s) after cancellation (which makes him pop up the notification again).
Why do you post cancellation on the main thread in the first place? Just check the status in uploadListener(int) and decide whether you want to update the notification or cancel it...
I've found solution to my problem in this stackoverflow thread.
When I changed NOTIFICATION_ID from 1 to [RANDOM_NUMBER], it magically started working. I assume that 1 is somehow reserved, although there is no note in any documentation...
An of course make sure you use the same NOTIFICATION_ID to cancel:
notificationManager.cancel(NOTIFICATION_ID);
I'm using Google Drive Android API (as part of Google Play Services) to upload files to cloud.
To connect client I'm using following code (simplified):
apiClient = new GoogleApiClient.Builder(context)
.addApi(Drive.API)
.setAccountName(preferences.getString("GOOGLE_DRIVE_ACCOUNT", null))
.build();
ConnectionResult connectionResult = apiClient.blockingConnect(SERVICES_CONNECTION_TIMEOUT_SEC, TimeUnit.SECONDS);
if (!connectionResult.isSuccess()) {
throw new ApiConnectionException(); //our own exception
}
To upload file I'm using something following code (simplified):
DriveApi.ContentsResult result = Drive.DriveApi.newContents(apiClient).await();
if (!result.getStatus().isSuccess()) {
/* ... code for error handling ... */
return;
}
OutputStream output = result.getContents().getOutputStream();
/* ... writing to output ... */
//create actual file on Google Drive
DriveFolder.DriveFileResult driveFileResult = Drive.DriveApi
.getFolder(apiClient, folderId)
.createFile(apiClient, metadataChangeSet, result.getContents())
.await();
Everything works as expected except for one particular user case. When user removes our app from "Connected apps" (using Google Settings application) this code still returns successful results for all invocations. Although file is never uploaded to Google Drive.
Connection to Google Play Services also succeeds.
Is it a bug of API or it can be somehow detected that user disconnected the application?
I don't know the API inside/out, however this page may help https://support.google.com/drive/answer/2523073?hl=en. I would double check the accounts.google.com page and verify that all permissions are being removed. This won't solve the api behaviour but at least you can verify the permissions.
Aren't you getting a UserRecoverableAuthIOException? Because you should. Any try to read/upload to drive where a user disconnected the app should return this Exception. You are probably catching general Exceptions and missing this. Try debugging to see if you aren't getting this exception.
If you are, all you have to do is re-request
catch (UserRecoverableAuthIOException e) {
startActivityForResult(e.getIntent(), COMPLETE_AUTHORIZATION_REQUEST_CODE);
}
And then deal with the response like this:
case COMPLETE_AUTHORIZATION_REQUEST_CODE:
if (resultCode == RESULT_OK) {
// App is authorized, you can go back to sending the API request
} else {
// User denied access, show him the account chooser again
}
break;
}
For creating a file try sending a IntentSender which according to this
By giving a IntentSender to another application, you are granting it the right to perform the operation you have specified as if the other application was yourself (with the same permissions and identity). Looks like a Pending Intent. You can create a file using
ResultCallback<ContentsResult> onContentsCallback =
new ResultCallback<ContentsResult>() {
#Override
public void onResult(ContentsResult result) {
// TODO: error handling in case of failure
MetadataChangeSet metadataChangeSet = new MetadataChangeSet.Builder()
.setMimeType(MIME_TYPE_TEXT).build();
IntentSender createIntentSender = Drive.DriveApi
.newCreateFileActivityBuilder()
.setInitialMetadata(metadataChangeSet)
.setInitialContents(result.getContents())
.build(mGoogleApiClient);
try {
startIntentSenderForResult(createIntentSender, REQUEST_CODE_CREATOR, null,
0, 0, 0);
} catch (SendIntentException e) {
Log.w(TAG, "Unable to send intent", e);
}
}
};
In here
`startIntentSenderForResult (IntentSender intent, int requestCode, Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags)`
If requestCode >= 0, this code will be returned in onActivityResult() when the activity exits.
in your onActivityResult() you can
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
//REQUEST_CODE_CREATOR == 1
case REQUEST_CODE_CREATOR:
if (resultCode == RESULT_OK) {
DriveId driveId = (DriveId) data.getParcelableExtra(
OpenFileActivityBuilder.EXTRA_RESPONSE_DRIVE_ID);
showMessage("File created with ID: " + driveId);
}
finish();
break;
default:
super.onActivityResult(requestCode, resultCode, data);
break;
}
}
Try to get the apiClient like this
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(Drive.API).addScope(Drive.SCOPE_FILE)
.setAccountName(mAccountName).addConnectionCallbacks(this)
.addOnConnectionFailedListener(this).build();
/**
* Called when {#code mGoogleApiClient} is connected.
*/
#Override
public void onConnected(Bundle connectionHint) {
Log.i(TAG, "GoogleApiClient connected");
}
/**
* Called when {#code mGoogleApiClient} is disconnected.
*/
#Override
public void onConnectionSuspended(int cause) {
Log.i(TAG, "GoogleApiClient connection suspended");
}
/**
* Called when {#code mGoogleApiClient} is trying to connect but failed.
* Handle {#code result.getResolution()} if there is a resolution is
* available.
*/
#Override
public void onConnectionFailed(ConnectionResult result) {
Log.i(TAG, "GoogleApiClient connection failed: " + result.toString());
if (!result.hasResolution()) {
GooglePlayServicesUtil.getErrorDialog(result.getErrorCode(), this, 0).show();
return;
}
try {
result.startResolutionForResult(this, REQUEST_CODE_RESOLUTION);
} catch (SendIntentException e) {
Log.e(TAG, "Exception while starting resolution activity", e);
}
}
You can get the mAccountName like this:
Account[] accounts = AccountManager.get(this).getAccountsByType("com.google");
if (accounts.length == 0) {
Log.d(TAG, "Must have a Google account installed");
return;
}
mAccountName = accounts[0].name;
Hope this helps.
I'm developing an Android app that uses the Dropbox Sync API to upload files. I have already created the app on Dropbox, gotten the APP_KEY and the APP_SECRET. I have included all the necessary libraries, set the proper keys in my activity code and the Manifest. My app is similar to the HelloDropbox sample provided in the documentation, but when I click on the "Link to Dropbox" button which is supposed to display a place to enter my dropbox credentials, nothing happens. Here's the source code:
package com.diamondtrust66.helix.player;
import java.io.File;
import java.io.IOException;
import java.util.List;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.dropbox.client2.DropboxAPI;
import com.dropbox.sync.android.DbxAccountManager;
import com.dropbox.sync.android.DbxFile;
import com.dropbox.sync.android.DbxFileInfo;
import com.dropbox.sync.android.DbxFileSystem;
import com.dropbox.sync.android.DbxPath;
public class HelixPlayer extends Activity {
private static final String appKey = "1234-my-key";
private static final String appSecret = "1234-my-secret";
private static final int REQUEST_LINK_TO_DBX = 0;
private TextView mTestOutput;
private Button mLinkButton;
private DbxAccountManager mDbxAcctMgr;
private DropboxAPI<?> mDBApi;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_helix_player);
mTestOutput = (TextView) findViewById(R.id.test_output);
mLinkButton = (Button) findViewById(R.id.link_button);
mLinkButton.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
onClickLinkToDropbox();
}
});
mDbxAcctMgr = DbxAccountManager.getInstance(getApplicationContext(), appKey, appSecret);
}
#Override
protected void onResume() {
super.onResume();
if (mDbxAcctMgr.hasLinkedAccount()) {
showLinkedView();
doDropboxTest();
} else {
showUnlinkedView();
}
}
private void showLinkedView() {
mLinkButton.setVisibility(View.GONE);
mTestOutput.setVisibility(View.VISIBLE);
}
private void showUnlinkedView() {
mLinkButton.setVisibility(View.VISIBLE);
mTestOutput.setVisibility(View.GONE);
}
private void onClickLinkToDropbox() {
mDbxAcctMgr.startLink((Activity)this, REQUEST_LINK_TO_DBX);
}
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_LINK_TO_DBX) {
if (resultCode == Activity.RESULT_OK) {
doDropboxTest();
} else {
Toast.makeText(getApplicationContext(), "FAILURE", Toast.LENGTH_LONG).show();
mTestOutput.setText("Link to Dropbox failed or was cancelled.");
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
private void doDropboxTest() {
try {
final String TEST_DATA = "Hello Dropbox";
final String TEST_FILE_NAME = "be like that.mp3";
DbxPath testPath = new DbxPath(DbxPath.ROOT, TEST_FILE_NAME);
// Create DbxFileSystem for synchronized file access.
DbxFileSystem dbxFs = DbxFileSystem.forAccount(mDbxAcctMgr.getLinkedAccount());
// Print the contents of the root folder. This will block until we can
// sync metadata the first time.
List<DbxFileInfo> infos = dbxFs.listFolder(DbxPath.ROOT);
mTestOutput.setText("\nContents of app folder:\n");
for (DbxFileInfo info : infos) {
mTestOutput.append(" " + info.path + ", " + info.modifiedTime + '\n');
}
// Create a test file only if it doesn't already exist.
if (!dbxFs.exists(testPath)) {
DbxFile testFile = dbxFs.create(testPath);
try {
File myFile = new File("/mnt/sdcard/alarms/be like that.mp3");
//testFile.writeString(TEST_DATA);
testFile.writeFromExistingFile(myFile, false);
} finally {
testFile.close();
}
mTestOutput.append("\nCreated new file '" + testPath + "'.\n");
}
// Read and print the contents of test file. Since we're not making
// any attempt to wait for the latest version, this may print an
// older cached version. Use getSyncStatus() and/or a listener to
// check for a new version.
/*if (dbxFs.isFile(testPath)) {
String resultData;
DbxFile testFile = dbxFs.open(testPath);
try {
resultData = testFile.readString();
} finally {
testFile.close();
}
mTestOutput.append("\nRead file '" + testPath + "' and got data:\n " + resultData);
} else if (dbxFs.isFolder(testPath)) {
mTestOutput.append("'" + testPath.toString() + "' is a folder.\n");
}*/
} catch (IOException e) {
mTestOutput.setText("Dropbox test failed: " + e);
}
}
}
Are you able to run the unmodified Hello Dropbox example on the same emulator/device where you're experiencing this problem? You can try replacing the app key/secret in the sample with your own as well. If those also fail, it may be there's something wrong with the configuration of your device which is keeping the API from launching a browser to complete authentication. If the example works, but your app doesn't, then I'd suspect something misconfigured there. Can you check with a log statement whether your call to startLink() is actually happening? Do you see anything appear in LogCat after that point?
The best way to debug this further might be to open a suppot ticket. Use the API Support link here: https://www.dropbox.com/developers
I ran into the same problem, the startLink() was doing nothing when I tried to use the same dropbox app credentials I was using with another android app my device had installed (although not running), but it didn't work. So you have two options: Uninstall any other android app using the same credentials OR create another dropbox app and renew the set of app/pass keys. Only then the Dropbox Login Dialog appears.