We recently started making use of the new Google Expansion APK mechanism. Overall it works well, but it seems somewhat flakey for us. Some questions:
Some users get the expansion app downloaded along with the app while others don't and our app has to download it itself. Does anyone know what governs when it works automatically and when not?
Sometimes when we need to download the expansion file ourselves, Google Play returns -1 for the file size and null for the URL, indicating the expansion file doesn't exist. If I run the app again, the second time it will generally return a valid size and URL. Does anyone else see this flakiness?
Here are the basics of the code:
This is how we set up the call to verify licensing via a callback
policy = new APKExpansionPolicy( context, new AESObfuscator( SALT, context.getPackageName(), deviceId ) );
mChecker = new LicenseChecker( context, policy, BASE64_PUBLIC_KEY );
mLicenseCheckerCallback = new MyLicenseCheckerCallback();
mChecker.checkAccess( mLicenseCheckerCallback );
Then in the callback we have this for the allow() method (when the license is valid).
public void allow( int reason )
{
String expansionFileName = policy.getExpansionFileName( APKExpansionPolicy.MAIN_FILE_URL_INDEX );
String expansionURL = policy.getExpansionURL( APKExpansionPolicy.MAIN_FILE_URL_INDEX );
long expansionFileSize = policy.getExpansionFileSize( APKExpansionPolicy.MAIN_FILE_URL_INDEX );
}
We just released the app with this new code, but a significant number of users are getting -1 back as the expansionFileSize and null as the url. This causes the user to not get the expansion file installed. Generally if they run the app again, it will work on the second (or third) time.
Anyone have any thoughts on what could be going on?
You are getting -1 because the APKExpansionPolicy responds with a local cached result if you try to contact the licensing server again - but the URL, filesize and filename are not cached and are lost after the first real response. APKExpansionPolicy does not cache these results, here is a comment from the APKExpansionPolicy source code which explains it:
Expansion URL's are not committed to preferences, but are instead intended to be stored when the license response is processed by the front-end.
So you need to store these values in the preferences right after you get the first successful response (in allow callback method);
The blog post on Android Developers addresses #1:
On most newer devices, when users download your app from Android Market, the expansion files will be downloaded automatically, and the refund period won’t start until the expansion files are downloaded. On older devices, your app will download the expansion files the first time it runs
To add to Daniel Novak's answer, if you reset the policy before the call to checkAccess(), this will force it to make a new license request, and therefore retrieve the URL:
policy.resetPolicy();
You probably only want to do this if you're sure you need the URL (ie, if you've already checked that the expansion file is missing).
Related
I have an Android app which uses its own folder on my Dropbox account. Every time I start up my app I get the below prompt:
Is it possible to code my app or configure it somehow so that I only have to acknowledge this prompt just once?
I don't mind having to acknowledge each time I change my code but every time the app is restarted seems a bit excessive.
I've reduced my code right down to one line which causes this to appear:
Auth.startOAuth2Authentication( gContext, APP_KEY );
Turns out I don't need to call:
Auth.startOAuth2Authentication( gContext, APP_KEY );
Once I get an access token, I can store that for subsequent program restarts and create a DbxClientV2 by using that saved token. I can also generate a token on my app's Dropbox webpage and use that instead (which makes more sense in my case anyway).
DbxRequestConfig config = new DbxRequestConfig( "dropbox/sample-app" );
DbxClientV2 client = new DbxClientV2( config, savedToken );
No more auth screen and the API calls continue to work!
I get an invalid parent folder error, and I've seen the solutions to use resource ID rather than Drive ID, but it's a different scenario here.
I'm trying to access the AppFolder, and this just uses the GoogleApiClient like so:
this.appFolder = Drive.DriveApi.getAppFolder(mGoogleApiClient);
When I later try to create a file in it, I get the above error.
DriveFolder.DriveFileResult fileResult = appFolder.createFile(mGoogleApiClient, changeSet, driveContentsResult.getDriveContents()).await();
Then fileResult.getStatus() gives me the erros.
This used to work for me before.
What's different is that I've manually emptied my app's data on Google Drive (delete hidden app data).
But I haven't disconnected the app - so I would assume that appFolder will continue to work the same way...
So far the only workaround is uninstalling the app, but this way I lose my data.
This is reproducible. Please help.
Update: This issue, #4483, was fixed in January 2017. The following fix may not apply anymore.
Since this continues to be an outstanding issue, I have taken steps to establish a work-around that can be done with code without resorting to user intervention.
As #seanpj says, this issue does not always occur and seems to be dependent upon the sync status of the Drive service. However, if the problem does occur, the following method works for me to circumvent the problem. I post it here in case it may be helpful to someone else. The Javadoc has more information.
/**
* Works around the Drive API for Android (GDAA) issue where the appDataFolder becomes
* unavailable after hidden app data is deleted through the online Drive interface
* (Settings->Manage Apps->Delete hidden app data) by using the REST v3 API to create an
* empty file in appDataFolder. The file is immediately deleted after creation but leaves
* the appDataFolder in working order.
* See <a href="https://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=4483"
* target="_blank">apps-api-issues #4483</a>
* <p>
* Call this method after a call to the Drive API fails with error code 1502
* (DriveStatusCodes.DRIVE_RESOURCE_NOT_AVAILABLE) when dealing with the appDataFolder then
* try the failed operation again.
* <p>
* This method assumes that all appropriate permissions have been granted and authorizations
* made prior to invocation.
*
* #param context - Context
* #param account The account name that has been authorized, e.g., someone#gmail.com.
* #throws IOException From the REST API.
*/
private void fixAppDataFolder(Context context, String account) throws IOException {
final String[] SCOPES = {DriveScopes.DRIVE_APPDATA};
GoogleAccountCredential credential = GoogleAccountCredential.usingOAuth2(
context, Arrays.asList(SCOPES)).setBackOff(new ExponentialBackOff());
HttpTransport transport = AndroidHttp.newCompatibleTransport();
JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
com.google.api.services.drive.Drive driveService;
credential.setSelectedAccountName(account);
driveService = new com.google.api.services.drive.Drive.Builder(
transport, jsonFactory, credential)
.setApplicationName("Your app name here")
.build();
File fileMetadata = new File();
fileMetadata.setName("fixAppDataFolder")
.setMimeType("text/plain")
.setParents(Collections.singletonList("appDataFolder"));
File appDataFile = driveService.files()
.create(fileMetadata)
.setFields("id")
.execute();
driveService.files().delete(appDataFile.getId()).execute();
} // fixAppDataFolder
Although this does not solve your problem, your question got me interested, so I ran a little test using this demo (follow "appfolder" in this code). Here's what I learned:
First, I could reproduce your problem using the following sequence
1/ getAppFolder(mGAC)
2/ create folder DEMOROOT in app folder
3/ create folder 2015-10 in DEMOROOT
4/ create file with content in 151022-075754 in 2015-10
5/ list full tree ... result \DEMOROOT\2015-10\151022-075754
6/ read content of 151022-075754 ... OK
so far so good. Without disconnecting, I go to
drive.google.com > Settings > Manage Apps > Options > Delete hidden app data
Now, there should be no objects in the app folder, I run:
1/ getAppFolder(mGAC)
2/ list full tree ... result \DEMOROOT\2015-10\151022-075754
3/ read content of 151022-075754 ... FAIL
As you can see, in my situation the getAppFolder(mGAC) returns valid DriveFolder (DriveId) that can be used. Even the DriveId string looks the same.
Listing of folders/files returns valid objects. It is not supposed to, but I know there is a latency I have to count on, so the list result may change later to reflect the deletion. Attempt to read the file content fails.
A few minutes later (GDAA probably synchronized), the same attempt to list fails, still understandable result, but another attempt to create any object (folder/file) in app folder fails with 'invalid parent folder' error as you pointed out. Disconnect / re-connect does not help, so the app is toasted.
This points to a serious bug that should be addressed. Again the same as in SO 30172915, an uneducated user's action can cause irreparable damage - loss of data to the Android App with no known remedy.
I am experiencing the same problem. I thought GoogleApiClient.ClearDefaultAccountAndReconnect() might be a workaround but it didn't help. As an alternative to uninstalling/reinstalling the app you can try these steps, they worked for me:
In a browser, go to the page for managing your Google account's security settings.
Select "Connected apps & sites", then "Manage Apps"
Select your app from the list. It will show that it has access to Google Drive and a Remove button.
Click on Remove.
Wait a minute or two for the change to take effect, then run your app.
When your app attempts to connect to Drive, you should get the consent screen prompting the user to allow or deny access. Accepting will reauthorize the app and should clear the problem. It did for me.
This bug was reported to google on May 4th:
https://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=4483
As a workaround you can use the REST API.
Working with Lollipop, I have a device-owner app that is installed with NFC at provision time.
What I need now is to handle automatic updates for my App, from Google Play to rely on the standard Android App update system...
So far I can imagine 2 ways to get this done, but don't know how to handle any of them :
in my NFC install constant EXTRA PROVISIONING DEVICE ADMIN PACKAGE
DOWNLOAD LOCATION install the App directly from the Play Store instead of the url on my own dev server. However
this constant need to handle the url of an apk file, and I did not find any
official way to get apk install direct from Play Store ? (as it will
be a production App in the future I'm not interested in hacks)
keep installing the apk from the dev server, but then allow the App
to update itself with its little brother located on the Play Store
with the same package name. To say it an other way: Would this be possible to install a v1 apk from a custom location, then put a v2 on the PlayStore... and let the magic come true ?
I'd be glad to hear if anyone could share experience about such procedures. Thanks for reading!
EDIT after #Stephan Branczyk suggestion I could make some more testing, here is what I did and the results:
1 - In the NFC provisioning I replaced the apk url with
snep://my.app.packagename without luck ; it just gives an error
without much explanation.
2 - I replaced this url by such a PlayStore link :
https://play.google.com/store/apps/details?id=my.app.packagename but
it gives a checksum error whether I use the checksum locally
computed, or the checksum given on the GooglePlay apk details. It looks not so far from the goal but I could not make it work.
3 - Finally I came back on my first solution, a self-hosted apk
versioned 1... but this time I tried to put on the PlayStore a newer
version 2 of the app with the exact same packagename... That led me
to strange things:
At first my App did not appear anywhere in the local PlayStore App,
but when I searched for it in Google Play, it showed up with the green
"installed" badge, and it proposed me to make an update... So did I.
Then, after this first manual update, the App is in v2, nice, and
better: it appears well listed in my PlayStore.
Optimistically, I uploaded a v3 of the App... just to see if my
PlayStore would automatically update my app (as is does for all the
other ones), but sadly no luck : even if my app is still listed in the
playstore, and proposing the "update" button... it never
updates by itself as it should ; I still need to click on it manually.
Isn't it a strange behavior ? If some have ideas about it, I would really need to be able to rely on the Play Store functionalities but so far no luck, and I cannot believe that Device-Owner app distribution is not compatible with PlayStore ?
Just in case, FYI here is the kind of provisioning code I'm using:
try {
Properties p = new Properties();
p.setProperty(
DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME,
"my.app.packagename");
p.setProperty(
DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION,
"http://www.example.com/myDeviceOwnerApp.apk");
p.setProperty(
DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM,
"U55o3fO0cXQtUoQCbQEO9c_gKrs");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
OutputStream out = new ObjectOutputStream(bos);
p.store(out, "");
final byte[] bytes = bos.toByteArray();
NdefMessage msg = new NdefMessage(NdefRecord.createMime(
DevicePolicyManager.MIME_TYPE_PROVISIONING_NFC, bytes));
return msg;
} catch (Exception e) {
throw new RuntimeException(e);
}
Write your package name as an AAR record in the tag.
To confirm that this functionality works, use this app to write the tag with.
You need to set Base64 encoded SHA1 or SHA256 (from M forward) of the apk in the
EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM
field when provisioning through NFC otherwise the provisioned device will not accept the URL for download.
Also see this answer for properly encoding the checksum.
I am experiencing an infuriating bug with the Google Drive API for Android. Specifically, in presenting an "Open File from Google Drive" UI to the user, I am querying for the files in a folder using the following code:
String query = "'" + folderId + "' in parents and trashed = false";
Files.List request = service.files().list().setQ(query);
FileList files = request.execute();
java.util.List<File> items = files.getItems(); // Returns null!
When I run this code directly from Eclipse (i.e. signed with the debug certificate), there is no problem. However, when I build and run a signed APK using our release certificate, the call to files.getItems() returns null.
Oddly, while getItems() returns null, there is definitely data returned from the Drive server, because calling files.toString() shows a mess of Json, amongst which I have identified the filenames of a few of my files, so I don't think the problem is an authentication issue.
Also, using different folderIds in the query string does not seem to make any difference to the Json returned by toString(). Based on aggressive GC activity in logcat, it looks like the server might be returning ALL files in my drive, especially odd considering getItems() returns null.
Note that the code works absolutely fine when signed with the debug certificate, as I am able to browse my Google Drive perfectly from within my app.
As per earlier comment:
Any chance Proguard kicks in when exporting your signed APK? If you rely on i.e. variable names to map the JSON onto POJOs, this is likely to brake without the appropriate Proguard exclusions/rules. Have a look in your project.properties file and comment out any lines in the form of proguard.config=<file_name>. After that, export another signed APK and retest.
I'm developing a non-public Android app, i.e. the app won't be available in the global Android Market. The app will be installed on a limited number of clients, e.g. by using an apk file.
How can I enable an auto-update functionality in this app?
I see different potential options (I do not know if those are technically hard or even impossible to implement or if there are any existing functionalities that can be reused):
On each launch the app tests if a new version exists (by requesting a server), if so downloads the new apk and replaces itself with the new version.
Use (or develop?) a separated app or service that undertakes the update-check and replacement-process.
Use (or develop?) a private market app which has an auto-update option. This option is similar to the second one, but more generic: The market app would be connected to a repository, i.e. it would handle an arbitrary number of (private) apps.
I would prefer option one since the auto-update functionality is included in the app which needs less development efforts.
janjonas, in the company I work we had a similar problem with Windows Mobile 6.x, and we use pretty much the same solution pointed by EboMike:
The main app check if it's updated, against a WebService. It receives the current version & the URL from where download the new version, if necessary. The main app then start the Updater app, passing the URL, and quit.
The Updater do the download of the new program, via HTTP, showing to the user the % downloaded. The user can cancel the download anytime, in a controlled way, and the Updater can registry this cancellation.
Since the new app is downloaded, the Updater run the new app, and quit.
I think option one is the least amount of work for you, and actually the cleanest one too since it will go through the proper channel of using Android's built-in package installer which includes user notification and the option for the user to abort the installation if desired.
You already have it all outlined - check for a new version on a server (would be nice to give the user the option to turn that off), and if there is a new version, you could either just link to the URL with the APK (which will, IIRC, use the browser's download manager to download it), or you could download it with your app and then point the intent to your local file. Using the HTTP link is technically less work and cleaner - the more you let the operating system do, the better - unless there's a reason not to.
Enabling "Install non-market app" is still needed for any application outside the Google Play. If it not enabled, the installation process is going to ask for it and redirect the user to the Application Settings, and after that, the user can install the app.
Depending on your needs, you can delegate to a third part lib.
Some of the permissions we'll use to get this done are the following:
<uses-permission android:name="android.permission.ACCESS_SUPERUSER" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
Let me explain a bit... The last, WRITE_EXTERNAL_STORAGE, is self-explanatory. With ACCESS_SUPERUSER we'll tell the system that we intend to use root privileges. READ_EXTERNAL_STORAGE will be needed in the future in order for your app to read files on SD card.
Assuming that you have downloaded the file and that all those devices can be rooted (limited number of clients, not on Play, etc.), you could do this:
String filePath = Environment.getExternalStorageDirectory().toString() + "/your_app_directory/your_app_filename.apk";
Process installProcess = null;
int installResult = -1337;
try {
installProcess = Runtime.getRuntime().exec("su -c pm install -r " + filePath);
} catch (IOException e) {
// Handle IOException the way you like.
}
if (installProcess != null) {
try {
installResult = installProcess.waitFor();
} catch(InterruptedException e) {
// Handle InterruptedException the way you like.
}
if (installResult == 0) {
// Success!
} else {
// Failure. :-/
}
} else {
// Failure 2. :-(
}
Here might be a very lame method but for some companies, if you believe its applicable, this might be very easy to implement.
Create an password screen (passwordActivity) that asks a password to access the application.
Once the password is entered, raise a flag (set a boolean value from false to true using sharedpreferences)
Place the .apk file on Google Store.
Change the password once everyone installs the app, and release a new update on Google Play Store.
Since the software is going to cache the flag value, the password screen won`t show up even the password is change. It will only show up for new installations so might need to repeat the process.
Note: This method might better fit if there is not hundreds of users using the application. And don`t forget this method is also not secure. To sum up, if you are looking a way to keep the application private and have no security concerns, this is what I recommend.
Update app
Make sure that you already have your new apk download on location
void installNewVersion(String location) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(new File(location + "app-debug.apk")),
"application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}