Thursday, July 28, 2011
Android Licensing - finished, at least.
Well, now that I've got the Google copy protection completed, the next step is what to do. It's easy enough if it gets accepted - just proceed as normal. If it comes through as non-licensed or other error, though, I guess the idea is going to be to basically push out a message and exit. Simple. Hopefully this will be a nice, short post.
Ok, so the server is set up as "NOT Licensed" for testing.
Here is what the dialog code will look like,
// Should not allow access. An app can handle as needed,
// typically by informing the user that the app is not licensed
// and then shutting down the app or limiting the user to a
// restricted set of features.
public void dontAllow() {
if (isFinishing()) {
// Don't update UI if Activity is finishing.
return;
}
// displayResult(getString(R.string.dont_allow));
displayLicensingCallbacResult("dont allow");
AlertDialog.Builder alertbox = new AlertDialog.Builder(StartActivity.this);
// set the message to display
alertbox.setMessage("The Google licensing server is unable to validate this copy of the application. Please contact support@kanjisoft.com");
// add a neutral button to the alert box and assign a click listener
alertbox.setNeutralButton("Ok", new DialogInterface.OnClickListener() {
// click listener on the alert box
public void onClick(DialogInterface arg0, int arg1) {
// the button was clicked
Toast.makeText(getApplicationContext(), "OK button clicked", Toast.LENGTH_LONG).show();
finish();
}
});
// show it
alertbox.show();
}
@Override
public void applicationError(ApplicationErrorCode errorCode) {
// TODO Auto-generated method stub
Log.d(StartActivity.TAG, "App error, code: " + errorCode);
Log.d(StartActivity.TAG, ">>>>>>>>>>>>> don't allow!");
// displayResult(getString(R.string.dont_allow));
displayLicensingCallbacResult("dont allow");
}
private void displayLicensingCallbacResult(final String result) {
mHandler.post(new Runnable() {
public void run() {
Toast.makeText(StartActivity.this, result,
Toast.LENGTH_LONG).show();
AlertDialog.Builder alertbox = new AlertDialog.Builder(StartActivity.this);
// set the message to display
alertbox.setMessage("The Google licensing server is unable to validate this copy of the application. Please retry or contact support@kanjisoft.com");
// add a neutral button to the alert box and assign a click listener
alertbox.setNeutralButton("Ok", new DialogInterface.OnClickListener() {
// click listener on the alert box
public void onClick(DialogInterface arg0, int arg1) {
// the button was clicked
Toast.makeText(getApplicationContext(), "OK button clicked", Toast.LENGTH_LONG).show();
finish();
}
});
// show it
alertbox.show();
}
});
}
}
Ok, here's the problem - the user can go ahead and start playing without regardless of the results of the check. Is it because this display is in a callback routine? If I let it set for a while, then the disallowed message pops up. But, before that happens, if they hit start and get into the question display, the invalid license dialog box doesn't show up until they get back to it.
That's good in a way, since it doesn't leave the valid customers hanging around waiting for the clearance. But why doesn't the dialog box pop up once it gets into the question display?
It might have something to do with this:
private void displayLicensingCallbackResult(final String result) {
mHandler.post(new Runnable() {
public void run() {
Toast.makeText(StartActivity.this, result,
Toast.LENGTH_LONG).show();
Android might not be able to see the mHandler, a member of StartActivity, until it comes back to the StartActivity display. Hmm...I could make the same call at the start of the question activity...then I would exit the whole app. Just make it part of the initialization routine. Is there a way to exit the whole app, like a system.exit?
Well, I guess system.exit isn't recommended. Per this thread:
http://stackoverflow.com/questions/2042222/android-close-application
It looks like the best way is just to call:
moveTaskToBack(true)
Ok, let's see if we can move this validation code to the initalize method.
Better yet, create an object for handling it so it will be nicely encapsulated functionally:
I can even make it an activity. But, how will that work if there's not corresponding display? And do I really want to make a display for this? Let's experiment and see what happens when we don't do a display.
Oh, yeah, can't forget to declare it in the manifest.
Ok, well, it's kind of dark. I could set up a background for it. I don't think there's any way I put it in the question display, I don't want that network lookup lag.
Hmm...even when I test it with the server tet to accept, it's rejecting it.
I'm starting to think it might be a good idea to have a global setting that's checked every time a question is asked. Yeah, that way, it won't slow things down, and yet it will be checked every time.
Ok, it's still showing up as unlicensed. Why, it worked fine yesterday. Does it matter what account I'm logged into on my phone?
Let me change the server to an app error message. I have a feeling something is getting cached somewhere. It's a real pain.
I'm logged onto the wireless on my phone..rather I *was*. Oh, yeah, I rebooted to try to clear the cache. Reconnecting. Rats. Same problem - same response, invalid license.
Here's the log...
/dalvikvm( 216): GC_CONCURRENT freed 889K, 49% free 3540K/6855K, external 1625K/2137K, paused 2ms+5ms
D/dalvikvm( 216): GC_CONCURRENT freed 707K, 49% free 3544K/6855K, external 1625K/2137K, paused 2ms+3ms
I/LicenseChecker( 941): Received response.
I/LicenseChecker( 941): Clearing timeout.
W/LicenseValidator( 941): An error has occurred on the licensing server.
Hmm? Ok, that's correct. That's the response I have set up. Well, I dunno. I might not have saved it - what was it before? Change it back to allowed. Ah, ok. There we go.
Ok, I think I need to make this an AsyncThread.
Actually, this should be a one-time check only. Once it works, it works. Set a flag and be done with it. Maybe show a message like "validating the license, please wait a few moment".
Ok, now it's working ok. Now, I just need to add to to either app state or just save it in local settings.
Ok, so, I've moved the validation call into the init function which is called in every activity. But, even before I call that, I'll just check the validated flag, which is stored in settings.
Ok, I may have had an error in my code earlier, and was displaying the unlicensed message even on the invalid server message.
Ok, now, it seems to be working ok in the unlicensed case. Let's try the licensed case...
Ok, it doesn't like calling an activity from outside an activity:
E/AndroidRuntime( 1918): java.lang.RuntimeException: Unable to resume activity {com.jlptquiz.app/com.jlptquiz.app.QuestionActivity}: android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
Well, I'm not in love with the idea. But what are the options? I want to call it from everywhere, and initialize is called from everywhere, but it's static. What's the harm? But what's this new task? I don't think I want a new task.
public static final int FLAG_ACTIVITY_NEW_TASK
Since: API Level 1
If set, this activity will become the start of a new task on this history stack. A task (from the activity that started it to the next task activity) defines an atomic group of activities that the user can move to. Tasks can be moved to the foreground and background; all of the activities inside of a particular task always remain in the same order. See Tasks and Back Stack for more information about tasks.
This flag is generally used by activities that want to present a "launcher" style behavior: they give the user a list of separate things that can be done, which otherwise run completely independently of the activity launching them.
When using this flag, if a task is already running for the activity you are now starting, then a new activity will not be started; instead, the current task will simply be brought to the front of the screen with the state it was last in. See FLAG_ACTIVITY_MULTIPLE_TASK for a flag to disable this behavior.
This flag can not be used when the caller is requesting a result from the activity being launched.
Constant Value: 268435456 (0x10000000)
Well, it looks like a menu kind of thing. Ok, what we can do for now is pull it out of initialization, back into the StartActivity. Then, just have QuestionActivity check the flag and finish if it's not licensed. That might do the trick.
Ok, then in QuestionActivity, we'll just check the flag:
if (!mAppState.licensced){
// will be checked in start activity
finish();
}
Ok, let's run this one. Good, seems to work ok. Now, I need to clear the valid license flag in my code. then set the server setting to unlicensed, and see what happens.
The question is, how to clear that field? It's a little tricky, I only want to clear it once. Maybe I'll use a boolean.
if (this.firstTimeTest){
this.firstTimeTest = false;
appState.licensed = false;
InitUtils.saveState(this);
}
Boy, how I would like to refactor into appState.save(); I'll get to it.
Ok, let's make sure this works first.
Ok, I saved the server to unlicensed. Now, let's run the test.
Good, looks good. Instead of the black screen, I'd like to give it a nicer looking background. I'll just use the start activity layout and take out all the data fields.
Ok, pretty good. I have my too-common dropped word in the message, need to fix that. Actually, I think the background might have been the start screen, I just didn't realize it, because it's kind of dark.
Ok, I'm liking this. Let's try it with a different error message. like ERROR_SERVER_FAILURE.
Wait. When I restart the app, it doesn't redo the check - it just sit's on the beginning display. Ah, It needs to be in onResume.
No, it is in onResume.
Well, there's something strange going on, like there are two displays...oh, yeah, I need to change the name of the layout in the check verification source code. Although I could leave it the same. I think the problem is that when I move it to back, it's coming back into the verification display, which doesn't do the check. Let's try this:
// click listener on the alert box
public void onClick(DialogInterface arg0,
int arg1) {
finish();
}
Where I add the finish, and see if it comes back into the start activity instead of the validation activity.
No, it's not quite working. What I should do is check the app status on return from that activity, and then just call finish.
How to do that? Ok here it is from SO:
Intent i = new Intent(this,CameraActivity.class);
startActivityForResult(i, STATIC_INTEGER_VALUE);
And this:
resultIntent = new Intent(null);
resultIntent.putExtra(PUBLIC_STATIC_STRING_IDENTIFIER, tabIndexValue);
setResult(Activity.RESULT_OK, resultIntent);
finish();
And this:
Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch(requestCode) {
case (STATIC_INTEGER_VALUE) : {
if (resultCode == Activity.RESULT_OK) {
int tabIndex = data.getIntExtra(PUBLIC_STATIC_STRING_IDENTIFIER);
// TODO Switch tabs using the index.
}
break;
}
}
}
Whew. Who would've thought a simple validation check could get so complicated? Why can't I just display a dialog?
I can just set a context in the licence checking class.
Hmm...when I try to do that, this:
private class MyLicenseCheckerCallback implements LicenseCheckerCallback {
has a problem with the "isFinishing", which is part of the Activity usually I guess. I guess I should pass the activity as an Activity instead of a Context.
Ok, we'll try it like this:
private void validateLicense() {
new ValidateLicense().valdidateLicense(this);
}
Ok, so, let's try this now.
Ah, yes. Much better. It's showing the same dialog, but this time against the real StartActivity layout. And it may not be necessary to show the "please wait", because it seems to go pretty quickly.
It seems a bit less safe to rely on a setting for the application. Maybe I should do it every time the app is restarted?
No, it's way to long. Also, I should put the validating message as a toast.long.
Well, now it's quick, even when I use task killer on it. Let's restart the phone.
It's a bit confusing now. It displayed the message quickly, I think, I when I first started it, but then the display went semi-dark and the app wouldn't react at all. Ok, I just did a number of restarts, and sometimes it's pretty slow. So, I'm only going to do it the once, when they first install the app.
This could really use a progress display - just that little circle....
Here we go, from http://developer.android.com/guide/topics/ui/dialogs.html#ProgressDialog
ProgressDialog dialog = ProgressDialog.show(MyActivity.this, "",
"Loading. Please wait...", true);
How about this:
ProgressDialog dialog = ProgressDialog.show(activity, "",
"Validating license, please wait a few moments...", true);
checkLicense();
dialog.cancel();
The cancel is no good due to the callback, which is async and therefore lets it fall through the cancel immediately. So, this works:
ProgressDialog dialog = ProgressDialog.show(activity, "",
"Validating license, please wait a few moments...", true);
checkLicense();
And we get a nice spinning wheel. Let's see if I can grab a picture of it.
Also shown at the beginning.
Ok, let's double-check it's working when the server returns a valid code...
Uhf. I accidentally hit something like back, and it seems to have gone into a loop displaying that progress circle...
Hmmm...I really need to figure out a way to cancel it.
It keeps getting this:
/vending ( 1124): [12] RequestRunnable.run(): Got ApiException from async request: HTTP 503 for http://android.clients.google.com/market/licensing/LicenseRequest
E/vending ( 1124): [12] RequestRunnable.run(): Got ApiException from async request: HTTP 503 for http://android.clients.google.com/market/licensing/LicenseRequest
E/vending ( 1124): [12] RequestRunnable.run(): Got ApiException from async request: HTTP 503 for http://android.clients.google.com/market/licensing/LicenseRequest
E/vending ( 1124): [12] RequestRunnable.run(): Got ApiException from async request: HTTP 503 for http://android.clients.google.com/market/licensing/LicenseRequest
D/dalvikvm( 1124): GC_CONCURRENT freed 520K, 52% free 3036K/6279K, external 1625K/2137K, paused 6ms+3ms
E/vending ( 1124): [12] RequestRunnable.run(): Got ApiException from async request: HTTP 503 for http://android.clients.google.com/market/li
Well, this could be the problem:
http://developer.android.com/guide/publishing/licensing.html
The licensing server applies general request limits to guard against overuse of resources that could result in denial of service. When an application exceeds the request limit, the licensing server returns a 503 response, which gets passed through to your application as a general server error. This means that no license response will be available to the user until the limit is reset, which can affect the user for an indefinite period.
What I don't get is why the 503 isn't passed through as some kind of an error. In fact, not of the codes except licensed return anything, except a call to dontAllow, which doesn't show any error codes.
Actually, the problem is, or part of the problem is that when the app is successufully verified, and the validation object goes out of scope, the progress indicator stays on, probably because it maybe runs in separate thread. The two ways around are 1) to cancel it on return or 2) set up a dialog box similar to the one for the denied result. I'll go for the second.
Ok, for some reason the dialog has to be called in this code:
private void displayLicensingCallbackResult(final String result) {
mHandler.post(new Runnable() {
public void run() {
AlertDialog.Builder alertbox = new AlertDialog.Builder(
ValidateLicense.this.mActivity);
etc.
Why does the handler have to be used here?
Alright. Let's give it a try. It's on success right now.
Oh, it's exiting the app. Since all the result displays are going through this:
private void displayLicensingCallbackResult(final String result) {
So, we need to check for not success:
if (!SUCCESS.equals(result)) {
ValidateLicense.this.mActivity.finish();
}
I'm not to keen on the not, but it's ok here, I suppose.
Ok, let's try again.
Nope, still not cancelled. I'll have to specifically do it.
Ok. Let's try this:
ValidateLicense.this.mProgressDialog.cancel();
if (SUCCESS.equals(result)) {
// noop
} else {
ValidateLicense.this.mActivity.finish();
}
Now let's double check the negative logic again, set the server to a negative response.
Cool, it came up with the error code. Pretty fast, too.
Ok, the success works fine. What happens when not connected?
I'll put in airplane mode and restart. I think it might cache the result or something.
Actually, it got a few of these:
/vending ( 2319): [13] RequestDispatcher.performRequestsOverNetwork(): IOException while performing API request: Connection to http://android.clients.google.com refused
E/vending ( 2319): [13] RequestRunnable.run(): Got IOException from async request: Connection to http://android.clients.google.com refused
W/vending ( 2319): [13] RequestDispatcher.performRequestsOverNetwork(): IOException while performing API request: Connection to http://an
It's too bad it doesn't give a more specific error on a timeout.
Ok. I think I've covered the basics here. I may improve on it later on, like sending it to the market or something. But for now, that's a wrap!
Subscribe to:
Post Comments (Atom)
No comments:
Post a Comment