I'm trying to get 'Change Subscriptions' to work using the Drive API for Android, but been unsuccessful so far.
Here the simple use case:
2 android devices, both using the same google account
both subscribe to the same 'file of interest' in their drive folder
if the file 'changes', be it from a change performed by one of the two devices or any external source, all devices that subscribed to this file are notified
As far as I understand, this is exactly what 'Change Subscriptions' are supposed to do for me. I'm using play services revision 27.
The problem I have:
A 'file content change' (or some other file event) made locally on one device is never properly propagated to the all other devices that subscribed to the same file.
Does anyone know of any solutions to this issue, or can point my to what I'm doing wrong?
I've written some simple testcode (see below), that only needs a connected googleApiClient, here's what I tested:
1.
device 1 creates a new testfile calling testFileWriteNew() and adds a change subscription to this file using testFileAddAndRemoveSubscription(), the expected log output:
testfile.txt created, driveId=DriveId:CAESABi0AyDAu9XZhVMoAA== resourceId=null
onCompletion; driveId=DriveId:CAESHDBCLXNzaGVuNGlURkFOMGh0ZWtGWU5FeHVTRVUYtAMgwLvV2YVTKAA= resourceId=0B-sshen4iTFAN0htekFYNExuSEU
STATUS_SUCCESS
added subscription to testfile.txt, driveId=DriveId:CAESHDBCLXNzaGVuNGlURkFOMGh0ZWtGWU5FeHVTRVUYtAMgwLvV2YVTKAA= resourceId=0B-sshen4iTFAN0htekFYNExuSEU
2.
device 2 adds a change subscription to the same file using testFileAddAndRemoveSubscription(), the expected log output:
added subscription to testfile.txt, driveId=DriveId:CAESHDBCLXNzaGVuNGlURkFOMGh0ZWtGWU5FeHVTRVUYwgIg9I-GyZRTKAA= resourceId=0B-sshen4iTFAN0htekFYNExuSEU
As expected, the driveId is different on both devices, but the resourceId is the same 0B-sshen4iTFAN0htekFYNExuSEU, so that same 'cloud' file is referenced
3.
If I update the file with some new data via testFileUpdate I get the following on device 1:
testfile.txt updated, driveId=DriveId:CAESHDBCLXNzaGVuNGlURkFOMGh0ZWtGWU5FeHVTRVUYtAMgwLvV2YVTKAA= resourceId=0B-sshen4iTFAN0htekFYNExuSEU
and device 2:
testfile.txt updated, driveId=DriveId:CAESHDBCLXNzaGVuNGlURkFOMGh0ZWtGWU5FeHVTRVUYwgIg9I-GyZRTKAA= resourceId=0B-sshen4iTFAN0htekFYNExuSEU
4.
Unfortunately, the 'change of content' in the onChange method of the service is only triggered locally. A changed done by device 1 never reaches device 2 and vice versa. If I update the file using device 2 I see the following log on device 2 coming from the service:
onChange; driveId=DriveId:CAESHDBCLXNzaGVuNGlURkFOMGh0ZWtGWU5FeHVTRVUYwgIg9I-GyZRTKAA= resourceId=0B-sshen4iTFAN0htekFYNExuSEU
contentChanged
onChange; driveId=DriveId:CAESHDBCLXNzaGVuNGlURkFOMGh0ZWtGWU5FeHVTRVUYwgIg9I-GyZRTKAA= resourceId=0B-sshen4iTFAN0htekFYNExuSEU
metadataChanged
but I never see the onChange method being triggered on device 1, if device 2 triggered a change, which I would expect.
Code:
private boolean testFileWriteNew() {
final DriveFolder folderRoot = Drive.DriveApi.getRootFolder(mGoogleApiClient);
DriveContentsResult contentsResult = Drive.DriveApi.newDriveContents(mGoogleApiClient).await();
if (!contentsResult.getStatus().isSuccess()) {
return false;
}
DriveContents originalContents = contentsResult.getDriveContents();
OutputStream os = originalContents.getOutputStream();
try {
os.write(String.valueOf(System.currentTimeMillis()).getBytes());
MetadataChangeSet originalMetadata = new MetadataChangeSet.Builder().setTitle("testfile.txt").setMimeType("text/plain").build();
// create the file in root
DriveFolder.DriveFileResult fileResult = folderRoot.createFile(mGoogleApiClient, originalMetadata, originalContents, new ExecutionOptions.Builder().setNotifyOnCompletion(true).build()).await();
if (!fileResult.getStatus().isSuccess()) {
return false;
}
// check 'locally created' file, not yet synced to drive
DriveResource.MetadataResult metadataResult = fileResult.getDriveFile().getMetadata(mGoogleApiClient).await();
if (!metadataResult.getStatus().isSuccess()) {
return false;
}
Log.d(TAG, "testfile.txt created, driveId=" + metadataResult.getMetadata().getDriveId().encodeToString() + " resourceId=" + metadataResult.getMetadata().getDriveId().getResourceId());
return true;
} catch (IOException ioe) {
return false;
}
}
private boolean testFileUpdate() {
final DriveFolder folderRoot = Drive.DriveApi.getRootFolder(mGoogleApiClient);
// find testfile
DriveId testFile = null;
MetadataBufferResult folderFilesSyncFolder = folderRoot.listChildren(mGoogleApiClient).await();
if (!folderFilesSyncFolder.getStatus().isSuccess()) {
return false;
} else {
MetadataBuffer bufferMetaData = folderFilesSyncFolder.getMetadataBuffer();
for(int i = 0; i < bufferMetaData.getCount(); ++i) {
final Metadata data = bufferMetaData.get(i);
if(!data.isFolder() && !data.isTrashed() && data.isEditable() && data.getTitle().equalsIgnoreCase("testfile.txt")) {
testFile = data.getDriveId();
break;
}
}
bufferMetaData.release();
}
if(testFile == null) {
return false;
}
// update testfile
DriveFile file = Drive.DriveApi.getFile(mGoogleApiClient, testFile);
DriveContentsResult driveContentsResult = file.open(mGoogleApiClient, DriveFile.MODE_WRITE_ONLY, null).await();
if (!driveContentsResult.getStatus().isSuccess()) {
return false;
}
DriveContents originalContents = driveContentsResult.getDriveContents();
OutputStream os = originalContents.getOutputStream();
try {
os.write(String.valueOf(System.currentTimeMillis()).getBytes());
// commit changes
com.google.android.gms.common.api.Status status = originalContents.commit(mGoogleApiClient, null).await();
if(!status.isSuccess()) {
return false;
}
Log.d(TAG, "testfile.txt updated, driveId=" + file.getDriveId().encodeToString() + " resourceId=" + file.getDriveId().getResourceId());
return true;
} catch (IOException ioe) {
return false;
}
}
private boolean testFileAddAndRemoveSubscription(boolean subscribe) {
final DriveFolder folderRoot = Drive.DriveApi.getRootFolder(mGoogleApiClient);
// find testfile
DriveId testFile = null;
MetadataBufferResult folderFilesSyncFolder = folderRoot.listChildren(mGoogleApiClient).await();
if (!folderFilesSyncFolder.getStatus().isSuccess()) {
return false;
} else {
MetadataBuffer bufferMetaData = folderFilesSyncFolder.getMetadataBuffer();
for(int i = 0; i < bufferMetaData.getCount(); ++i) {
final Metadata data = bufferMetaData.get(i);
if(!data.isFolder() && !data.isTrashed() && data.isEditable() && data.getTitle().equalsIgnoreCase("testfile.txt")) {
testFile = data.getDriveId();
break;
}
}
bufferMetaData.release();
}
if(testFile == null) {
return false;
}
// subscribe & unsubscribe
DriveFile file = Drive.DriveApi.getFile(mGoogleApiClient, testFile);
if(subscribe) {
com.google.android.gms.common.api.Status status = file.addChangeSubscription(mGoogleApiClient).await();
if(!status.isSuccess()) {
return false;
}
Log.d(TAG, "added subscription to testfile.txt, driveId=" + file.getDriveId().encodeToString() + " resourceId=" + file.getDriveId().getResourceId());
return true;
} else {
com.google.android.gms.common.api.Status status = file.removeChangeSubscription(mGoogleApiClient).await();
if(!status.isSuccess()) {
return false;
}
Log.d(TAG, "removed subscription from testfile.txt, driveId=" + file.getDriveId().encodeToString() + " resourceId=" + file.getDriveId().getResourceId());
return true;
}
}
And here the service class:
public class ChangeService extends DriveEventService {
// TAG
private static final String TAG = ChangeService.class.getSimpleName();
#Override
public void onChange(ChangeEvent event) {
final DriveId driveId = event.getDriveId();
Log.e(TAG, "onChange; driveId=" + driveId.encodeToString() + " resourceId=" + driveId.getResourceId());
if(event.hasContentChanged()) { Log.e(TAG, "contentChanged"); }
else if(event.hasMetadataChanged()) { Log.e(TAG, "metadataChanged"); }
else if(event.hasBeenDeleted()) { Log.e(TAG, "beenDeleted"); }
}
#Override
public void onCompletion(CompletionEvent event) {
final DriveId driveId = event.getDriveId();
Log.e(TAG, "onCompletion; driveId=" + driveId.encodeToString() + " resourceId=" + driveId.getResourceId());
switch (event.getStatus()) {
case CompletionEvent.STATUS_CONFLICT: Log.e(TAG, "STATUS_CONFLICT"); break;
case CompletionEvent.STATUS_FAILURE: Log.e(TAG, "STATUS_FAILURE"); break;
case CompletionEvent.STATUS_SUCCESS: Log.e(TAG, "STATUS_SUCCESS "); break;
case CompletionEvent.STATUS_CANCELED: Log.e(TAG, "STATUS_CANCELED "); break;
}
event.dismiss();
}
}
I believe, you are falling into the same trap as many of us did before. I too originally assumed that the 'DriveEventService' takes care of notifications between multiple devices running under the same account. I tried and failed miserably, see here (and notice the resounding silence - since April 2014). I was always getting events on a single device only. So, I actually realized that Change Events work only locally within the GooPlaySvcs instance.
This was more or less confirmed by a comment from Steve Bazyl in this unrelated answer (please read including the 'ORIGINAL POST' paragraph), confirming my theory that both 'Change Events' and 'Completion Events' are local (Completion Events report result of network action - like http response).
So to answer your question. after fighting this for awhile, I had to develop a different strategy:
1/ perform GDAA action (create, update)
2/ wait for a Completion Event indicating your mod has been promoted to the Drive
3/ broadcast GCM message that include ResourceId (not DriveId !) plus optional data (up to 4K) to the registered participants.
4/ 'Registered participants' react to the message and download updated metadata/content, resolving the conflicts.
This solution is from summer 2014 and there may be some other pre-packaged solutions from Google since. I'd be happy myself to hear from people who know if there is more elegant solution.
Quite frankly, I don't understand what is this and this for, if the Completion Events do not timely reflect (notify of) the update from another device.
Good Luck
Related
I have two observables(A, B), and I want the first to finish running before the second runs. But, that's not even the problem I'm having. The problem is that, when A is added before B, B doesn't run at all unless I place B before A then, the two runs. But, the scenario I'm in is like thus:
A - Pickup
B - Delivery
There are three types of orders. Pickup Only, Delivery Only and Pickup And Delivery. Pickups need to run before Deliveries in every situation. A Delivery only already have Pickup marked as true. A Pickup only, needs to be picked up and delivered on it being closed. Which is why I need Pickup to send all locally saved pickups first before sending deliveries. So, I did this:
Pickup
private Observable<UpdateMainResponse> getDeliveredOrders() {
String token = PrefUtil.getToken(context);
BehaviorSubject<Integer> pageControl = BehaviorSubject.create(1);
Observable<UpdateMainResponse> ret = pageControl.asObservable().concatMap(integer -> {
if (integer - 1 != deliveryUpdate.size()) {
Log.e(TAG, "DeliveredOrders: " + deliveryUpdate.size());
RealmOrderUpdate theDel = deliveryUpdate.get(integer-1);
Log.e(TAG, "DeliveryUpdate: " + theDel.toString());
DeliverOrder pickupOrder = new DeliverOrder();
pickupOrder.setUuid(theDel.getUuid());
pickupOrder.setCode(theDel.getDest_code());
pickupOrder.setDelivered_lat(theDel.getLoc_lat());
pickupOrder.setDelivered_long(theDel.getLoc_long());
return apiService.deliverOrder(theDel.getOrderId(), token, pickupOrder)
.subscribeOn(Schedulers.immediate())
.doOnNext(updateMainResponse -> {
try {
Log.e(TAG, updateMainResponse.toString());
realm.executeTransaction(realm1 -> theDel.deleteFromRealm());
} catch (Exception e) {
e.printStackTrace();
} finally {
pageControl.onNext(integer + 1);
}
});
} else {
return Observable.<UpdateMainResponse>empty().doOnCompleted(pageControl::onCompleted);
}
});
return Observable.defer(() -> ret);
}
Delivery
private Observable<UpdateMainResponse> getPickedOrders() {
Log.e(TAG, "PickedOrders: " + pickUpdate.size());
String token = PrefUtil.getToken(context);
BehaviorSubject<Integer> pageControl = BehaviorSubject.create(1);
Observable<UpdateMainResponse> ret = pageControl.asObservable().concatMap(integer -> {
Log.e(TAG, "MainPickedInteger: " + integer);
if (integer - 1 != pickUpdate.size()) {
RealmOrderUpdate thePick = pickUpdate.get(integer - 1);
Log.e(TAG, "PickedUpdate: " + thePick.toString());
PickupOrder pickupOrder = new PickupOrder();
pickupOrder.setUuid(thePick.getUuid());
pickupOrder.setCode(thePick.getSource_code());
pickupOrder.setPicked_lat(thePick.getLoc_lat());
pickupOrder.setPicked_long(thePick.getLoc_long());
return apiService.pickupOrder(thePick.getOrderId(), token, pickupOrder)
.subscribeOn(Schedulers.immediate())
.doOnNext(updateMainResponse -> {
try {
Log.e(TAG, updateMainResponse.toString());
realm.executeTransaction(realm1 -> thePick.deleteFromRealm());
} catch (Exception e) {
e.printStackTrace();
} finally {
pageControl.onNext(integer + 1);
}
});
} else {
return Observable.<UpdateMainResponse>empty().doOnCompleted(pageControl::onCompleted);
}
});
return Observable.defer(() -> ret);
}
Zipper
private Observable<ZipperResponse> batchedZip() {
return Observable.zip(getPickedOrders(), getDeliveredOrders(), (updateMainResponse, updateMainResponse2) -> {
List<UpdateMainResponse> orders = new ArrayList<>();
bakeries.add(updateMainResponse);
bakeries.add(updateMainResponse2);
return new ZipperResponse(orders);
});
}
Utilizing Zipper
public void generalUpload(APIRequestListener listener) {
batchedZip.subscribe(new Subscriber<ZipperResponse>() {
#Override
public void onCompleted() {
listener.didComplete();
unsubscribe();
}
#Override
public void onError(Throwable e) {
listener.handleDefaultError(e);
unsubscribe();
}
#Override
public void onNext(ZipperResponse zipperResponse) {
Log.e(TAG, zipperResponse.size());
}
});
}
Problem
I don't know why getDeliveredOrders() doesn't get called unless I move it to the first before getPickedOrders()
Reading through Rx Documentation for Zip I can see that it's not going to work as I expected where all of getPickedOrders() runs first before getDeliveredOrders() runs. It'll have to do it one by one. E.g: One of Pickup and then One of Delivery
Any help to understand what's going on would be appreciated. Thanks
Ok, so if I got that right:
Pickup only: need to run through the Pickup process, then they complete.
Delivery only: need to run through the Delivery process, then they complete.
Pickup and Delivery: need to run through Pickup first, then through Delivery.
On a very high level, almost preudo-code, why does this process not work?
Observable<Item> performPickup(Item item);
Observable<Item> performDelivery(Item item);
Observable<Items> items = ...;
items
.flatMap(item -> item.needsPickup() ? performPickup(item) : Observable.just(item))
.flatMap(item -> item.needsDelivery() ? performDelivery(item) : Observable.just(item))
.doOnNext(completedItem -> ...)
If you have different sources for the three types:
Observable<Item> items = Observable.merge(
pickupSource(),
deliverySource(),
pickupAndDeliverySource());
i use different savegames in my app. "coins" , "levels" , ...
It works fine but if a conflict detected then its wrong result.
/**
* Conflict resolution for when Snapshots are opened. Must be run in an AsyncTask or in a
* background thread,
*/
Snapshots.OpenSnapshotResult processSnapshotOpenResult(Snapshots.OpenSnapshotResult result, int retryCount) {
retryCount++;
int status = result.getStatus().getStatusCode();
Log.i(TAG, "Load Result for saveGame<" + savedGame.getName() + "> status: " + status);
if (status == GamesStatusCodes.STATUS_OK) {
return result;
} else if (status == GamesStatusCodes.STATUS_SNAPSHOT_CONTENTS_UNAVAILABLE) {
return result;
} else if (status == GamesStatusCodes.STATUS_SNAPSHOT_CONFLICT) {
saveResolveConflictGameData = true;
Log.i(TAG, "Konflikt aufgetreten");
Snapshots.OpenSnapshotResult resolveResult = null;
Snapshot snapshot = result.getSnapshot();
Snapshot conflictSnapshot = result.getConflictingSnapshot();
Snapshot mResolvedSnapshot = null;
mResolvedSnapshot = snapshot;
SnapshotMetadata s1Meta = snapshot.getMetadata();
SnapshotMetadata cMeta = conflictSnapshot.getMetadata();
// resolveConflict and get new merged Parser Object
//
Parser conflictParserTemp = savedGame.resolveConflict(snapshot, conflictSnapshot);
if ( conflictParserTemp == null) {
Log.e(TAG, "savedGame.resolveConflict(snapshot,conflictSnapshot) Error");
return result;
}
//
// wurde schon ein conflict behandelt ?
//
if ( conflictParser != null ) {
// merge previous Conflict with this conflict
conflictParser.merge(conflictParserTemp);
} else {
// set first conflict Parser
conflictParser = conflictParserTemp;
}
Log.i(TAG, String.format("Games.Snapshots.resolveConflict() Step %d", retryCount));
resolveResult =
Games.Snapshots.resolveConflict(
activity.mGoogleApiClient, result.getConflictId(), mResolvedSnapshot).await();
if (retryCount < MAX_SNAPSHOT_RESOLVE_RETRIES) {
// Recursively attempt again
return processSnapshotOpenResult(resolveResult, retryCount);
} else {
// Failed, log error and show Toast to the user
String message = "Could not resolve snapshot conflicts";
Log.e(TAG, message);
Toast.makeText(activity.getBaseContext(), message, Toast.LENGTH_LONG).show();
return resolveResult;
}
}
// Fail, return null.
return null;
}
The Error is that if I load savegame "coins" I become all conflicts from other savegames.
I see it here.
SnapshotMetadata s1Meta = snapshot.getMetadata();
SnapshotMetadata cMeta = conflictSnapshot.getMetadata();
The Snapshot for korrekt coins savegame show this:
SnapshotMetadataEntity{Game=GameEntity{ApplicationId=520840013521,
DisplayName=Crush me, PrimaryCategory=Simulation,
SecondaryCategory=null, Description=hallo, DeveloperName=steffen
höhmann, IconImageUri=null, IconImageUrl=null, HiResImageUri=null,
HiResImageUrl=null, FeaturedImageUri=null, FeaturedImageUrl=null,
PlayEnabledGame=true, InstanceInstalled=true,
InstancePackageName=cherry.de.wubbleburst, AchievementTotalCount=0,
LeaderboardCount=0, RealTimeMultiplayerEnabled=false,
TurnBasedMultiplayerEnabled=false, AreSnapshotsEnabled=true,
ThemeColor=00456B, HasGamepadSupport=false},
Owner=PlayerEntity{PlayerId=113260033482974102226,
DisplayName=shoehmi, HasDebugAccess=false, IconImageUri=null,
IconImageUrl=null, HiResImageUri=null, HiResImageUrl=null,
RetrievedTimestamp=1454003980807, Title=Anfänger,
LevelInfo=com.google.android.gms.games.PlayerLevelInfo#1e1b36},
SnapshotId=drive://113260033482974102226/520840013521/coins,
CoverImageUri=null, CoverImageUrl=null, CoverImageAspectRatio=0.0,
Description=null, LastModifiedTimestamp=1454004003382, PlayedTime=-1,
UniqueName=coins, ChangePending=true, ProgressValue=-1}
drive://113260033482974102226/520840013521/coins
and the snapshotData:
timestamp;coins#1453929273252;100#1453929280956;-70#230179;70
but he shows me savegame snaphot from "level" savegame as conflicted Snapshot:
levelId;points#1;3241#2;9634
and the Conflict Snapshot Metadata say it is a "coins" savegame:
SnapshotMetadataEntity{Game=GameEntity{ApplicationId=520840013521,
DisplayName=Crush me, PrimaryCategory=Simulation,
SecondaryCategory=null, Description=hallo, DeveloperName=steffen
höhmann, IconImageUri=null, IconImageUrl=null, HiResImageUri=null,
HiResImageUrl=null, FeaturedImageUri=null, FeaturedImageUrl=null,
PlayEnabledGame=true, InstanceInstalled=true,
InstancePackageName=cherry.de.wubbleburst, AchievementTotalCount=0,
LeaderboardCount=0, RealTimeMultiplayerEnabled=false,
TurnBasedMultiplayerEnabled=false, AreSnapshotsEnabled=true,
ThemeColor=00456B, HasGamepadSupport=false},
Owner=PlayerEntity{PlayerId=113260033482974102226,
DisplayName=shoehmi, HasDebugAccess=false, IconImageUri=null,
IconImageUrl=null, HiResImageUri=null, HiResImageUrl=null,
RetrievedTimestamp=1454003980807, Title=Anfänger,
LevelInfo=com.google.android.gms.games.PlayerLevelInfo#1e1b36},
SnapshotId=drive://113260033482974102226/520840013521/coins,
CoverImageUri=null, CoverImageUrl=null, CoverImageAspectRatio=0.0,
Description=null, LastModifiedTimestamp=1454004003382, PlayedTime=-1,
UniqueName=coins, ChangePending=true, ProgressValue=-1}
drive://113260033482974102226/520840013521/coins
Why only if conflict occured and without conflicts its running correct and
save / load correct??
Please Help me???
sorry for my english ;)
I've written an Android Wear application which receives an image wrapped in an Asset from a phone app using the Data API. The app used to work fine and has not been changed in ages but recently I started to find the image passed from the phone app was failing to be rendered on the screen of the wearable. On investigation I found that one of the methods, getFdForAsset was failing with a wearable status code of 4005 which means Asset Unavailable. See https://developers.google.com/android/reference/com/google/android/gms/wearable/WearableStatusCodes
I process data events in a call to my wearable app's onDataChanged method like this:
public void onDataChanged(DataEventBuffer dataEvents) {
LOGD(TAG, "XXXX MainActivity.onDataChanged()");
final List<DataEvent> events = FreezableUtils.freezeIterable(dataEvents);
dataEvents.close();
LOGD(TAG, "onDataChanged data event count=" + events.size());
for (DataEvent event : events) {
if (event.getType() == DataEvent.TYPE_CHANGED) {
String path = event.getDataItem().getUri().getPath();
if (IMAGE_PATH.equals(path)) {
DataMapItem dataMapItem = DataMapItem.fromDataItem(event.getDataItem());
LOGD(TAG, "onDataChanged getting image asset");
Asset photo = dataMapItem.getDataMap()
.getAsset(IMAGE_KEY);
LOGD(TAG, "onDataChanged photo asset="+photo);
final String toi_name = dataMapItem.getDataMap().getString(GYBO_NAME);
final String toi_info = dataMapItem.getDataMap().getString(GYBO_INFO);
current_toi_name = toi_name;
current_toi_info = toi_info;
LOGD(TAG, "onDataChanged TOI name="+toi_name);
LOGD(TAG, "onDataChanged TOI info="+toi_info);
Bitmap bitmap = loadBitmapFromAsset(google_api_client, photo);
And then attempt to create a Bitmap from the Asset in this method:
private Bitmap loadBitmapFromAsset(GoogleApiClient apiClient, Asset asset) {
if (asset == null) {
throw new IllegalArgumentException("XXXX Asset must be non-null");
}
DataApi.GetFdForAssetResult result = Wearable.DataApi.getFdForAsset(
apiClient, asset).await();
if (result == null) {
Log.w(TAG, "XXXX getFdForAsset returned null");
return null;
}
if (result.getStatus().isSuccess()) {
Log.d(TAG, "XXXX loadBitmapFromAsset getFdForAsset was successful");
} else {
Log.d(TAG, "XXXX loadBitmapFromAsset getFdForAsset was not successful. Error="+result.getStatus().getStatusCode()+":"+result.getStatus().getStatusMessage());
// Seeing status code 4005 here which means Asset Unavailable
}
InputStream assetInputStream = result.getInputStream();
if (assetInputStream == null) {
Log.w(TAG, "XXXX Requested an unknown Asset");
result.release();
return null;
}
result.release();
return BitmapFactory.decodeStream(assetInputStream);
}
The Asset object itself is not null, so it's coming across from the mobile app OK. And the path of the data event is being correctly recognised as being one which contains an image.
Does anyone have any idea as to why I'm getting this result and how to resolve it?
Thanks
one important thing... wearable as well as mobile module have to have the same signing certificate; just make sure if you define it via your build.gradle it's the same. this affects transferring assets... other data were synced w/o issues even with different certificates;
I was recently fighting with this issue and found this was the cause of ASSET_UNAVAILABLE, while adding wear module to existing app which had custom debug signing certificate defined in build.gradle - I had to have this certificate even for wearable for asset sync to work.
How are you sending the image? I found that if I used Asset.createFromUri(), it didn't work and gave me the ASSET UNAVAILABLE error. But when I switched to Asset.createFromFd(), it worked.
Here's the code that worked for me:
private static Asset createAssetFromBitmap(String imagePath) throws FileNotFoundException {
// creating from Uri doesn't work: gives a ASSET_UNAVAILABLE error
//return Asset.createFromUri(Uri.parse(imagePath));
final File file = new File(imagePath);
final ParcelFileDescriptor fd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
return Asset.createFromFd(fd);
}
We are using the built-in DownloadManager to grab files from our server. If we figure out that there has been an update to that file we delete the local version and re-queue a download from the DownloadManager. This only runs when you fully kill and re-start the app (timely updates to files are not the priority, just that we have all the files and that they get updated whenever we notice it). This system works perfectly on all of my personal testing devices, however, when testing in the api 19 emulator or on my co-worker's HTC One the files will download and then disappear (no longer in the app's external data folder). I've figured out that both are version 4.4.2 of android (where my devices are either 4.4.4 or 4.0.4). It's weird because they will stick around for a time, but then random files will disappear.
Here is some code:
AssetManager setup (setup of output folder)
private AssetManager(Context activity){
if(singleton != null&&IOUtils.hasExternalStorage() != IOUtils.ExtStorageState_OK){
return;
}
context = activity;
external = ContextCompat.getExternalFilesDirs(context, "")[0];
external.mkdirs();
imageFolder = new File(external,imagePath);
imageFolder.mkdirs();
singleton = this;
}
Download code
private static class DownloadObject {
public String ServerID;
public String updated_at;
public Uri image;
public DownloadObject() {
super();
}
public DownloadObject(String ServerID,String updated_at){
super();
this.ServerID = ServerID;
this.updated_at = updated_at;
}
public DownloadObject(Cursor cursor){
super();
this.ServerID = cursor.getString(cursor.getColumnIndex(ObjectDao.Properties.ServerID.columnName));
this.updated_at = cursor.getString(cursor.getColumnIndex(ObjectDao.Properties.UpdatedAt.columnName));
String imageFile = cursor.getString(cursor.getColumnIndex(ObjectDao.Properties.Image.columnName));
this.image = Uri.parse(AssetManager.getSingleton().getImageFolder().getPath()).buildUpon().appendPath(imageFile).scheme("file").build();
}
}
//downloadObjectVector is the fresh list of all objects from the server
//existingObjects is the Cursor from the db that lists all existing object locally
private void SpinOffDownloads(final Vector<DownloadObject> downloadObjectVector,final Cursor existingObjects){
new Thread(new Runnable() {
#Override
public void run() {
int count = 0;
if(existingObjects != null){
count = existingObjects.getCount();
}
if (count>0){
existingObjects.moveToFirst();
do{
final DownloadObject obj = new DownloadObject(existingObjects);
DownloadObject notNeededObject = ArrayUtils.findFirst(downloadObjectVector,new ArrayUtils.Predicate<DownloadObject>() {
#Override
public boolean evaluate(DownloadObject downloadObject) {
return downloadObject.ServerID.equals(obj.ServerID)&&downloadObject.updated_at.compareTo(obj.updated_at) <= 0;
}
});
if (notNeededObject != null){
File imageTest = null;
if(notNeededObject.image != null) {
Uri out = Uri.parse(AssetManager.getSingleton().getImageFolder().getPath()).buildUpon().appendPath(notNeededObject.image.getLastPathSegment()).scheme("file").build();
imageTest = new File(out.getPath());
}else{
Log.v(CLASS_NAME,"object with null image:"+notNeededObject.ServerID);
}
if (imageTest == null||imageTest.exists()) {
downloadObjectVector.remove(notNeededObject);
}else{
if (imageTest != null&&imageTest.exists()&&SHOULD_REPLACE_FILE){
Log.v(CLASS_NAME,"DELETING FILE(missing image):"+imageTest.getAbsolutePath());
imageTest.delete();
}
}
}else{
File imageTest = null;
if(obj.image != null) {
imageTest = new File(obj.image.getPath());
if (imageTest != null&&imageTest.exists()&&SHOULD_REPLACE_FILE){
Log.v(CLASS_NAME,"DELETING FILE(image):"+imageTest.getAbsolutePath());
imageTest.delete();
}
}else{
Log.v(CLASS_NAME,"object with null image:"+obj.ServerID);
}
}
}while(existingObjects.moveToNext());
}
if (existingObjects!= null){
try{
existingObjects.close();
}catch (Exception e){
}
}
DownloadManager dm = (DownloadManager)getSystemService(DOWNLOAD_SERVICE);
for (int i = 0; i < downloadObjectVector.size(); i++) {
try {
DownloadObject dlObj = downloadObjectVector.get(i);
Uri in = dlObj.image;
Uri out = Uri.parse(AssetManager.getSingleton().getImageFolder().getPath()).buildUpon().appendPath(in.getLastPathSegment()).scheme("file").build();
dm.enqueue(new DownloadManager.Request(in).setDestinationUri(out).setTitle(in.getLastPathSegment()));
}catch (Exception e){
Log.w(CLASS_NAME,"Error with Download queued:",e);
}
}
}
}).start();
}
Please let me know if you need any other information or code!
EDIT1
So I decided to elaborate on this a bit more with my testing for this and how the issue manifests itself in the hopes that it will make the picture that much more clear!
I start by loading the app via Android Studio and letting it run long enough to know that all the downloads finish and then I look through the app to see which images are there and which are missing. Most images are there normally. Next I exit the app and use the android task manager to fully kill it. Then I re-launch the app via Android Studio. I then wait to make sure that the downloads finish and watch the LogCat to see what files get deleted manually(normally a couple at maximum). Then I go through the app as see which images are still there/which have been added. It seems that every time new images appear AND new images disappear... And normally the ones that get marked as manually deleted actually get replaced via download properly(i.e. NOT "disappeared").
Please let me know if there are any tests you would like for me to do!
File Observer Test
First of all this is the first time I've used a FileObserver so if I've done something stupid please point it out. Here is my observer code:
external = ContextCompat.getExternalFilesDirs(context, null)[0];
external.mkdirs();
fileObserver = new FileObserver(external.getPath(),FileObserver.ALL_EVENTS) {
#Override
public void onEvent(final int event, final String relPath) {
String msg = "???";
switch (event){
case FileObserver.DELETE:
msg = "FILEOB DELETE relPath:"+relPath;
break;
case FileObserver.DELETE_SELF:
msg = "FILEOB DELETE_SELF relPath:"+relPath;
break;
case FileObserver.MODIFY:
msg = "FILEOB MODIFY relPath:"+relPath;
break;
case FileObserver.MOVE_SELF:
msg = "FILEOB MOVE_SELF relPath:"+relPath;
break;
case FileObserver.MOVED_TO:
msg = "FILEOB MOVED_TO relPath:"+relPath;
break;
case FileObserver.MOVED_FROM:
msg = "FILEOB MOVED_FROM relPath:"+relPath;
break;
case FileObserver.ATTRIB:
msg = "FILEOB ATTRIB relPath:"+relPath;
break;
case FileObserver.CREATE:
msg = "FILEOB CREATE relPath:"+relPath;
break;
default:
msg = "Unknown event:"+event+" at relPath:"+relPath;
}
fileObserverHandler.publish(new LogRecord(Level.INFO,msg));
fileObserverHandler.flush();
}
#Override
public void startWatching() {
super.startWatching();
fileObserverHandler.publish(new LogRecord(Level.INFO,"START WATCHING!!!!"));
fileObserverHandler.flush();
Log.v("FileObserver","START WATCHING!!!");
}
};
fileObserver.startWatching();
I'm using the handler because at first I didn't have the startWatching() override in and wasn't getting any logging at all and the docs say that onEvent happens on its own thread and therefore you should use a handler. It's simply this in the class:
public static Handler fileObserverHandler = new ConsoleHandler();
The ONLY output I get from this at all is "START WATCHING!!!". So I'm guessing I must have done something wrong, because I see it downloading/deleting things... at least it says it is.
The behavior you describe sounds like the system is clearing up those files like a cache.
In your call to getExternalFilesDirs you use "", trying to creating a File/directory with "" can be problematic.
Use null instead of "" in your call to getExternalFilesDirs see if that helps
replace
external = ContextCompat.getExternalFilesDirs(context, "")[0];
with
external = ContextCompat.getExternalFilesDirs(context, null)[0];
It seems this issue may not be related to version 4.4.2 only. After reviewing the Download code over and over, I noticed that the download request does not have a setMimeType setting. Sometimes it appears that DownloadManager deletes files upon completion without setting mime type to download request, on some occasions. By default the server sends the file as its content type as application/x-download. Try adding something like
setMimeType(application/octet-stream);
to DownloadManager.Request(in) or whichever mime type that suits the files being downloaded. Hope this helps.
I think it isn't a problem related with the application logic, but the device you were testing on. I have a tablet with the same problem and I was going crazy... the internal storage (were I save the files) may be damaged...
I am creating a generic Chromecast remote control app. Most of the guts of the app are already created and I've managed to get Chromecast volume control working (by connecting to a Chromecast device along side another app that is casting - YouTube for example).
What I've having difficult with is performing other media commands such as play, pause, seek, etc.
Use case example:
1. User opens YouTube on their android device and starts casting a video.
2. User opens my app and connects to the same Chromecast device.
3. Volume control from my app (works now)
4. Media control (play, pause, etc) (does not yet work)
I found the Cast api reference that explains that you can sendMessage(ApiClient, namespace, message) with media commands; however the "message" (JSON) requires the sessionId of the current application (Youtube in this case). I have tried the following, but the connection to the current application always fails; status.isSuccess() is always false:
Cast.CastApi
.joinApplication(mApiClient)
.setResultCallback(
new ResultCallback<Cast.ApplicationConnectionResult>() {
#Override
public void onResult(
Cast.ApplicationConnectionResult result) {
Status status = result.getStatus();
if (status.isSuccess()) {
ApplicationMetadata applicationMetadata = result
.getApplicationMetadata();
sessionId = result.getSessionId();
String applicationStatus = result
.getApplicationStatus();
boolean wasLaunched = result
.getWasLaunched();
Log.i(TAG,
"Joined Application with sessionId: "
+ sessionId
+ " Application Status: "
+ applicationStatus);
} else {
// teardown();
Log.e(TAG,
"Could not join application: "
+ status.toString());
}
}
});
Is is possible to get the sessionId of an already running cast application from a generic remote control app (like the one I am creating)? If so, am I right in my assumption that I can then perform media commands on the connected Chromecast device using something like this:
JSONObject message = new JSONObject();
message.put("mediaSessionId", sessionId);
message.put("requestId", 9999);
message.put("type", "PAUSE");
Cast.CastApi.sendMessage(mApiClient,
"urn:x-cast:com.google.cast.media", message.toString());
Update:
I have tried the recommendations provided by #Ali Naddaf but unfortunately they are not working. After creating mRemoteMediaPlayer in onCreate, I also do requestStatus(mApiClient) in the onConnected callback (in the ConnectionCallbacks). When I try to .play(mApiClient) I get an IllegalStateException stating that there is no current media session. Also, I tried doing joinApplication and in the callback performed result.getSessionId; which returns null.
A few comments and answers:
You can get the sessionId from the callback of launchApplication or joinApplication; in the "onResult(result)", you can get the sessionId from: result.getSessionId()
YouTube is still not on the official SDK so YMMV, for apps using official SDK, you should be able to use the above approach (most of it)
Why are you trying to set up a message yourself? Why not building a RemoteMediaPlayer and using play/pause that is provided there? Whenever you are working with the media playback through the official channel, always use the RemoteMediaPlayer (don't forget to call requestStatus() on it after creating it).
Yes it is possible , First you have to save sesionId and CastDevice device id
and when remove app from background and again open app please check is there sessionId then call bello line.
Cast.CastApi.joinApplication(apiClient, APP_ID,sid).setResultCallback(connectionResultCallback);
if you get success result then need to implement further process in connectionResultCallback listener.
//Get selected device which you selected before
#Override
public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo route) {
// Log.d("Route Added", "onRouteAdded");
/* if (router.getRoutes().size() > 1)
Toast.makeText(homeScreenActivity, "'onRouteAdded :: " + router.getRoutes().size() + " -- " + router.getRoutes().get(1).isSelected(), Toast.LENGTH_SHORT).show();
else
Toast.makeText(homeScreenActivity, "'onRouteAdded :: " + router.getRoutes(), Toast.LENGTH_SHORT).show();*/
if (router != null && router.getRoutes() != null && router.getRoutes().size() > 1) {
// Show the button when a device is discovered.
// Toast.makeText(homeScreenActivity, "'onRouteAdded :: " + router.getRoutes().size() + " -- " + router.getRoutes().get(1).isSelected(), Toast.LENGTH_SHORT).show();
mMediaRouteButton.setVisibility(View.VISIBLE);
titleLayout.setVisibility(View.GONE);
castName.setVisibility(View.VISIBLE);
selectedDevice = CastDevice.getFromBundle(route.getExtras());
routeInfoArrayList = router.getRoutes();
titleLayout.setVisibility(View.GONE);
if (!isCastConnected) {
String deid = MyPref.getInstance(homeScreenActivity).readPrefs(MyPref.CAST_DEVICE_ID);
for (int i = 0; i < routeInfoArrayList.size(); i++) {
if (routeInfoArrayList.get(i).getExtras() != null && CastDevice.getFromBundle(routeInfoArrayList.get(i).getExtras()).getDeviceId().equalsIgnoreCase(deid)) {
selectedDevice = CastDevice.getFromBundle(routeInfoArrayList.get(i).getExtras());
routeInfoArrayList.get(i).select();
ReSelectedDevice(selectedDevice, routeInfoArrayList.get(i).getName());
break;
}
}
}
}
}
//Reconnect google Api Client
public void reConnectGoogleApiClient() {
if (apiClient == null) {
Cast.CastOptions apiOptions = new
Cast.CastOptions.Builder(selectedDevice, castClientListener).build();
apiClient = new GoogleApiClient.Builder(this)
.addApi(Cast.API, apiOptions)
.addConnectionCallbacks(reconnectionCallback)
.addOnConnectionFailedListener(connectionFailedListener)
.build();
apiClient.connect();
}
}
// join Application
private final GoogleApiClient.ConnectionCallbacks reconnectionCallback = new GoogleApiClient.ConnectionCallbacks() {
#Override
public void onConnected(Bundle bundle) {
// Toast.makeText(homeScreenActivity, "" + isDeviceSelected(), Toast.LENGTH_SHORT).show();
try {
String sid = MyPref.getInstance(homeScreenActivity).readPrefs(MyPref.CAST_SESSION_ID);
String deid = MyPref.getInstance(homeScreenActivity).readPrefs(MyPref.CAST_DEVICE_ID);
if (sid != null && deid != null && sid.length() > 0 && deid.length() > 0)
Cast.CastApi.joinApplication(apiClient, APP_ID, sid).setResultCallback(connectionResultCallback);
isApiConnected = true;
} catch (Exception e) {
}
}
#Override
public void onConnectionSuspended(int i) {
isCastConnected = false;
isApiConnected = false;
}
};