I have a strange issue which I cannot explain. In the manifest file the launch activity of my app is defined as follows:
<activity
android:name="com.xxx.xxx.xxx.StartupActivity"
android:label="#string/app_name"
android:theme="#android:style/Theme.Light.NoTitleBar"
android:screenOrientation="sensorPortrait" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
In the StartUpActivity the following check is performed:
protected void startIntent() {
Intent intent;
if (checkCurrentProfile()) {
Notifier.showHttpToast(R.string.toastLoggedIn);
//try to update the device token in the database
Request.updateDeviceToken();
intent = new Intent(this, GameListActivity.class);
} else {
intent = new Intent(this, RegisterActivity.class);
}
startActivity(intent);
finish();
}
So if the user has a valid account the GameListActivity is shown as root activity:
<activity
android:name="com.xxx.xxx.xxx.xxx.GameListActivity"
android:label="#string/app_name"
android:theme="#style/MyTheme"
android:screenOrientation="sensorPortrait" >
</activity>
The issue now is the following: sometimes the system brings the root activity to the front spontaneously without any user actions. It only occurs sometimes, but I can't figure out the cause. Can anyone help me out here?
The StartUpActivity looks like this:
public class StartupActivity extends StartupCoreActivity implements OnRegisterGCMListener {
private IabHelper mHelper;
private IabHelper.QueryInventoryFinishedListener mQueryInventoryFinishedListener;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
registerOnRegisterGCMListener(this);
if (!registerGCMneeded()) {
initInAppBilling();
}
}
#Override
public void registerGCMfinished() {
initInAppBilling();
}
private void initInAppBilling() {
boolean hasPremium = Prefs.getBoolValue(getResources().getString(R.string.pref_key_upgrade_premium), false);
if (hasPremium) {
//unlocking contents not needed
startIntent();
} else {
Prefs.storeValue(getResources().getString(R.string.pref_key_upgrade_premium), false);
mHelper = new IabHelper(this, C.Billing.BILLING_KEY);
mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
public void onIabSetupFinished(IabResult result) {
if (result.isSuccess()) {
queryInventory();
} else {
startIntent();
}
}
});
}
}
private void queryInventory() {
String[] products = {C.Billing.ITEM_SKU};
mQueryInventoryFinishedListener = new IabHelper.QueryInventoryFinishedListener() {
#Override
public void onQueryInventoryFinished(IabResult result, Inventory inv) {
if (result.isSuccess()) {
checkPremiumVersion(inv);
} else {
startIntent();
}
}
};
mHelper.queryInventoryAsync(true, Arrays.asList(products), mQueryInventoryFinishedListener);
}
private void checkPremiumVersion(Inventory inv) {
if (inv.hasPurchase(C.Billing.ITEM_SKU)) {
Request.updPremiumVersion();
Prefs.storeValue(getResources().getString(R.string.pref_key_upgrade_premium), true);
Notifier.showHttpToast(R.string.toastPremiumContentsUnlocked);
}
startIntent();
}
}
And the StartupCoreActivity looks like this:
public class StartupCoreActivity extends Activity {
private final static int PLAY_SERVICES_RESOLUTION_REQUEST = xxxx;
GoogleCloudMessaging mGcm;
Context mContext;
String mRegId;
/**
* Substitute you own sender ID here. This is the project number you got
* from the API Console, as described in "Getting Started."
*/
String SENDER_ID = "xxxxxxxxxx";
private OnRegisterGCMListener mOnRegisterGCMListener = null;
public void registerOnRegisterGCMListener(OnRegisterGCMListener listener) {
mOnRegisterGCMListener = listener;
}
protected boolean registerGCMneeded() {
mContext = getApplicationContext();
// Check device for Play Services APK.
if (checkPlayServices()) {
// If this check succeeds, proceed with normal processing.
// Otherwise, prompt user to get valid Play Services APK.
mGcm = GoogleCloudMessaging.getInstance(this);
mRegId = getRegistrationId(mContext);
if (mRegId.isEmpty()) {
registerInBackground();
return true;
} else {
// note we never called setContentView()
return false;
}
}
return false;
}
protected void startIntent() {
Intent intent;
if (checkCurrentProfile()) {
Notifier.showHttpToast(R.string.toastLoggedIn);
//try to update the device token in the database
Request.updateDeviceToken();
intent = new Intent(this, GameListActivity.class);
} else {
intent = new Intent(this, RegisterActivity.class);
}
startActivity(intent);
finish();
}
private boolean checkCurrentProfile() {
KUPlayer me = G.getMySelf();
if (me.getPlayerId() <= 0) {
//database was not present yet and has been created
//or database was present, but profile cannot be read anymore
// make sure Login screen appears ONLY if PlayerID cannot be retrieved anymore
if (G.getPlayerID() <=0) {
Prefs.storeValue(Prefs.PREF_KEY_PWD_SAVED, false);
return false;
}
}
return true;
}
// You need to do the Play Services APK check here too.
#Override
protected void onResume() {
super.onResume();
checkPlayServices();
}
/**
* Check the device to make sure it has the Google Play Services APK. If
* it doesn't, display a dialog that allows users to download the APK from
* the Google Play Store or enable it in the device's system settings.
*/
private boolean checkPlayServices() {
int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
if (resultCode != ConnectionResult.SUCCESS) {
if (GooglePlayServicesUtil.isUserRecoverableError(resultCode)) {
GooglePlayServicesUtil.getErrorDialog(resultCode, this,
PLAY_SERVICES_RESOLUTION_REQUEST).show();
} else {
finish();
}
return false;
}
return true;
}
/**
* Registers the application with mGcm servers asynchronously.
* <p>
* Stores the registration ID and the app versionCode in the application's
* shared preferences.
*/
private void registerInBackground() {
new AsyncTask<Void, Void, String>() {
#Override
protected String doInBackground(Void... params) {
String msg = "";
try {
if (mGcm == null) {
mGcm = GoogleCloudMessaging.getInstance(mContext);
}
mRegId = mGcm.register(SENDER_ID);
msg = "Device registered, registration ID=" + mRegId;
// Persist the mRegId - no need to register again.
storeRegistrationId(mContext, mRegId);
} catch (IOException ex) {
msg = "Error :" + ex.getMessage();
// If there is an error, don't just keep trying to register.
// Require the user to click a button again, or perform
// exponential back-off.
}
return msg;
}
#Override
protected void onPostExecute(String msg) {
// note we never called setContentView()
if (mOnRegisterGCMListener != null) {
mOnRegisterGCMListener.registerGCMfinished();
}
}
}.execute(null, null, null);
}
/**
* #return Application's version code from the {#code PackageManager}.
*/
private static int getAppVersionCode(Context context) {
try {
PackageInfo packageInfo = context.getPackageManager()
.getPackageInfo(context.getPackageName(), 0);
return packageInfo.versionCode;
} catch (NameNotFoundException e) {
// should never happen
throw new RuntimeException("Could not get package name: " + e);
}
}
private static String getAppVersionName(Context context) {
try {
PackageInfo packageInfo = context.getPackageManager()
.getPackageInfo(context.getPackageName(), 0);
return packageInfo.versionName;
} catch (NameNotFoundException e) {
// should never happen
throw new RuntimeException("Could not get package name: " + e);
}
}
/**
* Stores the registration ID and the app versionCode in the application's
* {#code SharedPreferences}.
*
* #param mContext application's mContext.
* #param mRegId registration ID
*/
private void storeRegistrationId(Context context, String regId) {
Prefs.storeValue(Prefs.PREF_KEY_DEVICE_TOKEN, regId);
Prefs.storeValue(Prefs.PREF_KEY_APP_VERSION_CODE, getAppVersionCode(context));
Prefs.storeValue(Prefs.PREF_KEY_APP_VERSION_NAME, getAppVersionName(context));
AppRate.resetAfterUpdate();
}
/**
* Gets the current registration ID for application on mGcm service, if there is one.
* <p>
* If result is empty, the app needs to register.
*
* #return registration ID, or empty string if there is no existing
* registration ID.
*/
private String getRegistrationId(Context context) {
String registrationId = Prefs.getStringValue(Prefs.PREF_KEY_DEVICE_TOKEN);
if (registrationId.isEmpty()) {
return "";
}
// Check if app was updated; if so, it must clear the registration ID
// since the existing mRegId is not guaranteed to work with the new
// app version.
int registeredVersion = Prefs.getIntValue(Prefs.PREF_KEY_APP_VERSION_CODE);
int currentVersion = getAppVersionCode(context);
if (registeredVersion != currentVersion) {
return "";
}
return registrationId;
}
}
I guess it has to do with the following known Android bug:
How to prevent multiple instances of an activity when it is launched with different intents
And as suggested put the following code in the onCreate method of the rootActivity:
// Possible work around for market launches. See http://code.google.com/p/android/issues/detail?id=2373
// for more details. Essentially, the market launches the main activity on top of other activities.
// we never want this to happen. Instead, we check if we are the root and if not, we finish.
if (!isTaskRoot()) {
final Intent intent = getIntent();
final String intentAction = intent.getAction();
if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && intentAction != null && intentAction.equals(Intent.ACTION_MAIN)) {
finish();
return;
}
}
I tested it and when you start your app from Google Play store in stead of launch screen, then indeed finish() is called from above code.
Related
I've been working on an Android app that uses QuickBlox for chatting. The chat is working fine, but I am encountering difficulties with the push notifications for logged out users.
The problem is that some devices sometimes stop receiving notifications. It often works fine for a couple of hours, but then suddenly completely stops working for that device. A full manual uninstall + reinstall (which then logs the user in again to QuickBlox + GCM) usually solves the problem.
Looking at the QuickBlox admin panel, I can see that some subscriptions lose their push token.
User 1 registered on device A. Push token has disappeared.
http://i.stack.imgur.com/YUkyw.png
User 1 registered on device B. Push token still present.
http://i.stack.imgur.com/Yu5IQ.png
I register with GCM + QuickBlox push messages once the user is logged in to QuickBlox. This code was largely taken from the QuickBlox chat sample.
ChatService:
private void loginToChat(final Context context, final QBUser user, final QBEntityCallback callback)
{
this.qbChatService.login(user, new QBEntityCallbackImpl()
{
#Override
public void onSuccess()
{
checkPlayServices(context);
try
{
qbChatService.startAutoSendPresence(AUTO_PRESENCE_INTERVAL_IN_SECONDS);
}
catch (SmackException.NotLoggedInException e)
{
e.printStackTrace();
}
if (callback != null)
{
callback.onSuccess();
}
}
#Override
public void onSuccess(Object result, Bundle params)
{
super.onSuccess(result, params);
checkPlayServices(context);
}
#Override
public void onError(List errors)
{
if (callback != null)
{
callback.onError(errors);
}
}
});
}
private void checkPlayServices(Context context)
{
// Register with GCM after the session has been created
PlayServicesHelper playServicesHelper = new PlayServicesHelper(context);
playServicesHelper.checkPlayServices();
}
PlayServicesHelper:
public class PlayServicesHelper
{
private static final String PROPERTY_APP_VERSION = "appVersion";
private static final String PROPERTY_REG_ID = "registration_id";
private static final String TAG = "PlayServicesHelper";
private static final int PLAY_SERVICES_RESOLUTION_REQUEST = 9000;
private GoogleCloudMessaging googleCloudMessaging;
private Context activity;
private String regId;
private String projectNumber;
public PlayServicesHelper(Context activity)
{
this.activity = activity;
this.projectNumber = activity.getString(R.string.gcm_project_number);
checkPlayService();
}
private void checkPlayService()
{
// Check device for Play Services APK. If check succeeds, proceed with
// GCM registration.
if (checkPlayServices())
{
googleCloudMessaging = GoogleCloudMessaging.getInstance(activity);
regId = getRegistrationId();
if (regId.isEmpty())
{
registerInBackground();
}
}
else
{
Log.i(TAG, "No valid Google Play Services APK found.");
}
}
/**
* Check the device to make sure it has the Google Play Services APK. If
* it doesn't, display a dialog that allows users to download the APK from
* the Google Play Store or enable it in the device's system settings.
*/
public boolean checkPlayServices()
{
int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(activity);
if (resultCode != ConnectionResult.SUCCESS)
{
if (GooglePlayServicesUtil.isUserRecoverableError(resultCode))
{
GooglePlayServicesUtil.getErrorDialog(resultCode, activity, PLAY_SERVICES_RESOLUTION_REQUEST).show();
}
return false;
}
return true;
}
/**
* Gets the current registration ID for application on GCM service.
* <p/>
* If result is empty, the app needs to register.
*
* #return registration ID, or empty string if there is no existing
* registration ID.
*/
private String getRegistrationId()
{
final SharedPreferences prefs = getGCMPreferences();
String registrationId = prefs.getString(PROPERTY_REG_ID, "");
if (registrationId.isEmpty())
{
Log.i(TAG, "Registration not found.");
return "";
}
// Check if app was updated; if so, it must clear the registration ID
// since the existing regID is not guaranteed to work with the new
// app version.
int registeredVersion = prefs.getInt(PROPERTY_APP_VERSION, Integer.MIN_VALUE);
int currentVersion = getAppVersionCode();
if (registeredVersion != currentVersion)
{
Log.i(TAG, "App version changed.");
return "";
}
return registrationId;
}
/**
* Registers the application with GCM servers asynchronously.
* <p/>
* Stores the registration ID and app versionCode in the application's
* shared preferences.
*/
private void registerInBackground()
{
new AsyncTask<Void, Void, String>()
{
#Override
protected String doInBackground(Void... params)
{
String msg = "";
try
{
if (googleCloudMessaging == null)
{
googleCloudMessaging = GoogleCloudMessaging.getInstance(activity);
}
regId = googleCloudMessaging.register(projectNumber);
msg = "Device registered, registration ID=" + regId;
// You should send the registration ID to your server over HTTP, so it
// can use GCM/HTTP or CCS to send messages to your app.
Handler h = new Handler(activity.getMainLooper());
h.post(new Runnable()
{
#Override
public void run()
{
subscribeToPushNotifications(regId);
}
});
// Persist the regID - no need to register again.
storeRegistrationId(regId);
}
catch (IOException ex)
{
msg = "Error :" + ex.getMessage();
// If there is an error, don't just keep trying to register.
// Require the user to click a button again, or perform
// exponential back-off.
}
return msg;
}
#Override
protected void onPostExecute(String msg)
{
Log.i(TAG, msg + "\n");
}
}.execute(null, null, null);
}
/**
* #return Application's {#code SharedPreferences}.
*/
private SharedPreferences getGCMPreferences()
{
return activity.getSharedPreferences(activity.getPackageName(), Context.MODE_PRIVATE);
}
/**
* Subscribe to Push Notifications
*
* #param regId registration ID
*/
private void subscribeToPushNotifications(String regId)
{
String deviceId;
final TelephonyManager mTelephony = (TelephonyManager) activity.getSystemService(Context.TELEPHONY_SERVICE);
if (mTelephony.getDeviceId() != null)
{
deviceId = mTelephony.getDeviceId();
}
else
{
deviceId = Settings.Secure.getString(activity.getContentResolver(), Settings.Secure.ANDROID_ID);
}
QBMessages.subscribeToPushNotificationsTask(regId, deviceId, QBEnvironment.PRODUCTION, new QBEntityCallbackImpl<ArrayList<QBSubscription>>()
{
#Override
public void onSuccess(ArrayList<QBSubscription> qbSubscriptions, Bundle bundle)
{
}
#Override
public void onError(List<String> strings)
{
}
});
}
/**
* Stores the registration ID and app versionCode in the application's
* {#code SharedPreferences}.
*
* #param regId registration ID
*/
private void storeRegistrationId(String regId)
{
final SharedPreferences prefs = getGCMPreferences();
int appVersion = getAppVersionCode();
SharedPreferences.Editor editor = prefs.edit();
editor.putString(PROPERTY_REG_ID, regId);
editor.putInt(PROPERTY_APP_VERSION, appVersion);
editor.apply();
}
private int getAppVersionCode()
{
try
{
PackageInfo packageInfo = this.activity.getPackageManager().getPackageInfo(this.activity.getPackageName(), 0);
return packageInfo.versionCode;
}
catch (PackageManager.NameNotFoundException e)
{
return 0;
}
}
}
To allow a user to receive notifications, I log him out every time the app goes to the background (using onStop in a BaseActivity) and then log back in when the app resumes (which then again starts the PlayServicesHelper).
ChatService.getInstance().logout(null);
The app also uses Parse (with GCM) for push notifications, but the problem still seemed to occur when I disabled Parse.
Could the problem be caused by having one user registered on multiple devices at the same time? Could the frequent app installs (without manually removing the previous installation) result in this behavior?
Push token can disappear if 2 users use the same device
for example, User 1 subscribed for pushes on deviceA
Then User 2 subscribed for pushes on deviceA (the same device)
After this only User 2 will be receiving pushes, not User 1
Is there a chance to have such logic in your case?
I've been given access to the blogger API, I've confirmed that on my developer console.
I've also written some code to perform oAuth2 with Google Play Services using some of the code below.
String SCOPE ="oauth2:https://www.googleapis.com/auth/blogger";
GoogleAuthUtil.getToken(context, "myEmail#gmail.com", mScope);
It returns a token. As it should.
However, once I try to access the api using the token i get a error.
Unexpected response code 403 for https://www.googleapis.com/blogger/v3/users/self/blogs
Here is my request:
And here is my response:
Here is my BaseActivity.java code that gets the token:
public class BaseActivity extends Activity {
static final int REQUEST_CODE_PICK_ACCOUNT = 1000;
static final int REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR = 1001;
static final int REQUEST_CODE_RECOVER_FROM_AUTH_ERROR = 1002;
private static final String SCOPE ="oauth2:https://www.googleapis.com/auth/blogger";
private String mEmail; // Received from newChooseAccountIntent(); passed to getToken()
public ProgressDialog mDialog;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mDialog = new ProgressDialog(this);
login();
}
public void login() {
pickUserAccount();
}
private void pickUserAccount() {
String[] accountTypes = new String[]{"com.google"};
Intent intent = AccountPicker.newChooseAccountIntent(null, null, accountTypes, false, null, null, null, null);
startActivityForResult(intent, REQUEST_CODE_PICK_ACCOUNT);
}
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_PICK_ACCOUNT) {
// Receiving a result from the AccountPicker
if (resultCode == RESULT_OK) {
mEmail = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
// With the account name acquired, go get the auth token
getToken();
} else if (resultCode == RESULT_CANCELED) {
// The account picker dialog closed without selecting an account.
// Notify users that they must pick an account to proceed.
Toast.makeText(this, "Pick Account", Toast.LENGTH_SHORT).show();
}
} else if ((requestCode == REQUEST_CODE_RECOVER_FROM_AUTH_ERROR ||
requestCode == REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR)
&& resultCode == RESULT_OK) {
// Receiving a result that follows a GoogleAuthException, try auth again
getToken();
}
}
private void getToken() {
if (mEmail == null) {
pickUserAccount();
} else {
if (isDeviceOnline()) {
new getTokenTask(BaseActivity.this, mEmail, SCOPE).execute();
} else {
Toast.makeText(this, "Not online", Toast.LENGTH_LONG).show();
}
}
}
/**
* This method is a hook for background threads and async tasks that need to
* provide the user a response UI when an exception occurs.
*/
public void handleException(final Exception e) {
// Because this call comes from the AsyncTask, we must ensure that the following
// code instead executes on the UI thread.
runOnUiThread(new Runnable() {
#Override
public void run() {
if (e instanceof GooglePlayServicesAvailabilityException) {
// The Google Play services APK is old, disabled, or not present.
// Show a dialog created by Google Play services that allows
// the user to update the APK
int statusCode = ((GooglePlayServicesAvailabilityException)e).getConnectionStatusCode();
Dialog dialog = GooglePlayServicesUtil.getErrorDialog(statusCode, BaseActivity.this, REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR);
dialog.show();
} else if (e instanceof UserRecoverableAuthException) {
// Unable to authenticate, such as when the user has not yet granted
// the app access to the account, but the user can fix this.
// Forward the user to an activity in Google Play services.
Intent intent = ((UserRecoverableAuthException)e).getIntent();
startActivityForResult(intent, REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR);
}
}
});
}
public boolean isDeviceOnline() {
ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isConnected()) {
return true;
} else {
return false;
}
}
public class getTokenTask extends AsyncTask{
Activity mActivity;
String mScope;
String mEmail;
getTokenTask(Activity activity, String name, String scope) {
this.mActivity = activity;
this.mScope = scope;
this.mEmail = name;
}
#Override
protected Object doInBackground(Object[] params) {
try {
String token = fetchToken();
Preferences.saveString(Constants.KEY_BLOGGER_TOKEN, token);
} catch (IOException e) {
// The fetchToken() method handles Google-specific exceptions,
// so this indicates something went wrong at a higher level.
// TIP: Check for network connectivity before starting the AsyncTask.
}
return null;
}
/**
* Gets an authentication token from Google and handles any
* GoogleAuthException that may occur.
*/
protected String fetchToken() throws IOException {
try {
return GoogleAuthUtil.getToken(mActivity, mEmail, mScope);
} catch (UserRecoverableAuthException userRecoverableException) {
// GooglePlayServices.apk is either old, disabled, or not present
// so we need to show the user some UI in the activity to recover.
((BaseActivity)mActivity).handleException(userRecoverableException);
} catch (GoogleAuthException fatalException) {
// Some other type of unrecoverable exception has occurred.
// Report and log the error as appropriate for your app.
}
return null;
}
}
}
I've been banging my head against the wall on this one. Anyone have any ideas?
Finally figured it out.
My build.gradle file somehow ended up having a different Application ID than my manifest. I changed it so they both match the manifest, and boom! it worked.
I am trying to implement the com.google.android.gms.common.api.GoogleApiClient in my project.
The problem is that every time I try to connect, I get the call back to the onConnectionFailed listener with a pending intent that I execute. On a clean install, the very first pending intent will launch an account select screen. This is expected. Every subsequent restart of the app will bypass the account selection, unless the app's data is cleared in the Application Manager.
After the account-select screen, the signing-in screen will appear breifly. It never signs in though. The onActivityResult will be called after the signing-in screen flashes, which tries to connect the client. It doesn't connect, and calls the onConnectionFailed listener again.
If I keep trying to execute the intents, I get stuck in loop with the signing in screen breifly appearing, then disappearing, but never connecting or getting signed in. The ConnectionResult.toString indicates "Sign_In_Required", and returns an error code of 4 (the same as the Sign_In_Required constant.
On the API console, I've implemented an Ouath 2.0 client ID, and a public API access key for android applications. Notably, my app works using the older com.google.api.services.drive.Drive client.
As for my code:
I've tried using two different implementations here and here. I tried to implement the second example making as few changes as possible. It is reproduced below:
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.newContents(mGoogleApiClient).setResultCallback(new ResultCallback<DriveApi.ContentsResult>() {
#Override
public void onResult(DriveApi.ContentsResult 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.getContents().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)
.setInitialContents(result.getContents())
.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 happens because after the first login/authorization android keeps using the same default account parameters. If you want to avoid the loop and make sure the picker shows again you must clear completely the default account by calling Plus.AccountApi.clearDefaultAccount(mGoogleApiClient) before reconnecting again.
To achieve this, you must add the Plus.API scope to the GoogleApiClient builder:
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(Drive.API)
.addApi(Plus.API)
.addScope(Drive.SCOPE_FILE)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
And then you can clear the default account before rebuilding the api client and connecting to a different account (rebuilding the api client when changing accounts avoids problems):
// if the api client existed, we terminate it
if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
Plus.AccountApi.clearDefaultAccount(mGoogleApiClient);
mGoogleApiClient.disconnect();
}
// build new api client to avoid problems reusing it
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(Drive.API)
.addApi(Plus.API)
.addScope(Drive.SCOPE_FILE)
.setAccountName(account)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
mGoogleApiClient.connect();
No additional permissions or api activations are needed for using the Plus.API scope this way. I hope this helps with your problem.
It is a tough one, since I don't have time to completely re-run and analyze your code. And without running it, I don't see anything obvious.
But, since I have this stuff up and running in my app, I'd like to help. Unfortunately the Google Play services connection and authorization code is scattered all over my app's fragments and activities. So, I made an attempt to create a dummy activity and pull all the stuff in it. By 'all the stuff' I mean the account manager wrapper (GA) and associated account picker code.
The result is some 300 lines of gibberish that may work, but I don't make any claims it will. Take a look and good luck.
package com.......;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentSender;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.widget.Toast;
import com.google.android.gms.auth.GoogleAuthUtil;
import com.google.android.gms.common.AccountPicker;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.common.api.GoogleApiClient;
public class GooApiClient extends Activity implements
GoogleApiClient.OnConnectionFailedListener, GoogleApiClient.ConnectionCallbacks {
private static final String DIALOG_ERROR = "dialog_error";
private static final String REQUEST_CODE = "request_code";
private static final int REQ_ACCPICK = 1;
private static final int REQ_AUTH = 2;
private static final int REQ_RECOVER = 3;
private GoogleApiClient mGooApiClient;
private boolean mIsInAuth; //block re-entrancy
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (checkPlayServices() && checkUserAccount()) {
gooInit();
gooConnect(true);
}
}
#Override
public void onConnected(Bundle bundle) {
Log.d("_", "connected");
}
#Override
public void onConnectionSuspended(int i) { }
#Override
public void onConnectionFailed(ConnectionResult result) {
Log.d("_", "failed " + result.hasResolution());
if (!mIsInAuth) {
if (result.hasResolution()) {
try {
mIsInAuth = true;
result.startResolutionForResult(this, REQ_AUTH);
} catch (IntentSender.SendIntentException e) {
suicide("authorization fail");
}
} else {
suicide("authorization fail");
}
}
}
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent it) {
Log.d("_", "activity result " + requestCode + " " + resultCode);
switch (requestCode) {
case REQ_AUTH: case REQ_RECOVER: {
mIsInAuth = false;
if (resultCode == Activity.RESULT_OK) {
gooConnect(true);
} else if (resultCode == RESULT_CANCELED) {
suicide("authorization fail");
}
return;
}
case REQ_ACCPICK: { // return from account picker
if (resultCode == Activity.RESULT_OK && it != null) {
String emil = it.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
if (GA.setEmil(this, emil) == GA.CHANGED) {
gooInit();
gooConnect(true);
}
} else if (GA.getActiveEmil(this) == null) {
suicide("selection failed");
}
return;
}
}
super.onActivityResult(requestCode, resultCode, it); // DO NOT REMOVE
}
private boolean checkPlayServices() {
Log.d("_", "check PS");
int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
if (status != ConnectionResult.SUCCESS) {
if (GooglePlayServicesUtil.isUserRecoverableError(status)) {
mIsInAuth = true;
errorDialog(status, LstActivity.REQ_RECOVER);
} else {
suicide("play services failed");
}
return false;
}
return true;
}
private boolean checkUserAccount() {
String emil = GA.getActiveEmil(this);
Account accnt = GA.getPrimaryAccnt(this, true);
Log.d("_", "check user account " + emil + " " + accnt);
if (emil == null) { // no emil (after install)
if (accnt == null) { // multiple or no accounts available, go pick one
accnt = GA.getPrimaryAccnt(this, false);
Intent it = AccountPicker.newChooseAccountIntent(accnt, null,
new String[]{GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE}, true, null, null, null, null
);
this.startActivityForResult(it, LstActivity.REQ_ACCPICK);
return false; //--------------------->>>
} else { // there's only one goo account registered with the device, skip the picker
GA.setEmil(this, accnt.name);
}
// UNLIKELY BUT POSSIBLE, emil's OK, but the account have been removed since (through settings)
} else {
accnt = GA.getActiveAccnt(this);
if (accnt == null) {
accnt = GA.getPrimaryAccnt(this, false);
Intent it = AccountPicker.newChooseAccountIntent(accnt, null,
new String[]{GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE}, true, null, null, null, null
);
this.startActivityForResult(it, LstActivity.REQ_ACCPICK);
return false; //------------------>>>
}
}
return true;
}
private void gooInit(){
String emil = GA.getActiveEmil(this);
Log.d("_", "goo init " + emil);
if (emil != null){
mGooApiClient = new GoogleApiClient.Builder(this)
.setAccountName(emil).addApi(com.google.android.gms.drive.Drive.API)
.addScope(com.google.android.gms.drive.Drive.SCOPE_FILE)
.addConnectionCallbacks(this).addOnConnectionFailedListener(this)
.build();
}
}
private void gooConnect(boolean bConnect) {
Log.d("_", "goo connect " + bConnect);
if (mGooApiClient != null) {
if (!bConnect) {
mGooApiClient.disconnect();
} else if (! (mGooApiClient.isConnecting() || mGooApiClient.isConnected())){
mGooApiClient.connect();
}
}
}
private void suicide(String msg) {
GA.removeActiveAccnt(this);
Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
finish();
}
private void errorDialog(int errorCode, int requestCode) {
Bundle args = new Bundle();
args.putInt(DIALOG_ERROR, errorCode);
args.putInt(REQUEST_CODE, requestCode);
ErrorDialogFragment dialogFragment = new ErrorDialogFragment();
dialogFragment.setArguments(args);
dialogFragment.show(getFragmentManager(), "errordialog");
}
public static class ErrorDialogFragment extends DialogFragment {
public ErrorDialogFragment() { }
#Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
int errorCode = getArguments().getInt(DIALOG_ERROR);
int requestCode = getArguments().getInt(DIALOG_ERROR);
return GooglePlayServicesUtil.getErrorDialog(errorCode, getActivity(), requestCode);
}
#Override
public void onDismiss(DialogInterface dialog) {
getActivity().finish();
}
}
private static class GA {
private static final String ACC_NAME = "account_name";
public static final int FAIL = -1;
public static final int UNCHANGED = 0;
public static final int CHANGED = +1;
private static String mCurrEmil = null; // cache locally
private static String mPrevEmil = null; // cache locally
public static Account[] getAllAccnts(Context ctx) {
return AccountManager.get(acx(ctx)).getAccountsByType(GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE);
}
public static Account getPrimaryAccnt(Context ctx, boolean bOneOnly) {
Account[] accts = getAllAccnts(ctx);
if (bOneOnly)
return accts == null || accts.length != 1 ? null : accts[0];
return accts == null || accts.length == 0 ? null : accts[0];
}
public static Account getActiveAccnt(Context ctx) {
return emil2Accnt(ctx, getActiveEmil(ctx));
}
public static String getActiveEmil(Context ctx) {
if (mCurrEmil != null) {
return mCurrEmil;
}
mCurrEmil = ctx == null ? null : pfs(ctx).getString(ACC_NAME, null);
return mCurrEmil;
}
public static Account getPrevEmil(Context ctx) {
return emil2Accnt(ctx, mPrevEmil);
}
public static Account emil2Accnt(Context ctx, String emil) {
if (emil != null) {
Account[] accounts =
AccountManager.get(acx(ctx)).getAccountsByType(GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE);
for (Account account : accounts) {
if (emil.equalsIgnoreCase(account.name)) {
return account;
}
}
}
return null;
}
/**
* Stores a new email in persistent app storage, reporting result
* #param newEmil new email, optionally null
* #param ctx activity context
* #return FAIL, CHANGED or UNCHANGED (based on the following table)
* OLD NEW SAVED RESULT
* ERROR FAIL
* null null null FAIL
* null new new CHANGED
* old null old UNCHANGED
* old != new new CHANGED
* old == new new UNCHANGED
*/
public static int setEmil(Context ctx, String newEmil) {
int result = FAIL; // 0 0
mPrevEmil = getActiveEmil(ctx);
if ((mPrevEmil == null) && (newEmil != null)) {
result = CHANGED;
} else if ((mPrevEmil != null) && (newEmil == null)) {
result = UNCHANGED;
} else if ((mPrevEmil != null) && (newEmil != null)) {
result = mPrevEmil.equalsIgnoreCase(newEmil) ? UNCHANGED : CHANGED;
}
if (result == CHANGED) {
mCurrEmil = newEmil;
pfs(ctx).edit().putString(ACC_NAME, newEmil).apply();
}
return result;
}
public static void removeActiveAccnt(Context ctx) {
mCurrEmil = null;
pfs(ctx).edit().remove(ACC_NAME).apply();
}
private static Context acx(Context ctx) {
return ctx == null ? null : ctx.getApplicationContext();
}
private static SharedPreferences pfs(Context ctx) {
return ctx == null ? null : PreferenceManager.getDefaultSharedPreferences(acx(ctx));
}
}
}
BTW, I know how to spell 'email', 'Emil' just happened to be my uncle's name and I couldn't resist :-)
UPDATE (2015-Apr-11):
I've recently re-visited the code that handles Google Drive Authorization and Account switching. The result can be found here and it supports both REST and GDAA apis.
I set up the "Dungeons" InAppBilling example locally and I am ready to try it out, but I am a bit confused. I have a button like this:
Button donate = (Button)findViewById(R.id.donate);
donate.setOnClickListener(new Button.OnClickListener() {
public void onClick(View v) {
// But what do I do here? :)
}
});
And when it is called, what do I need to do to actually go to the pay screen on android store?
Thanks!
I better suggest you to use this code as this example is quiet simple and easy to handle at first .. the things you have to do is
http://blog.blundell-apps.com/simple-inapp-billing-payment/
download the sample project code from the above link (Having Description of code and download link at bottom)
in your android project where you want to implement in app billing, create package com.android.vending.billing and place IMarketBillingService.aidl (you can find this and all files mention below in the project you downloaded in step 1)
place following utility files in any package and correct import statements accordingly.
* BillingHelper.java
* BillingReceiver.java
* BillingSecurity.java
* BillingService.java
* C.java
place the public key (you can find it in developer console in the bottom section of edit profile) in the BillingSecurity.java in line saying String base64EncodedPublicKey = "your public key here"
Declare the following permission (outside the application tag), service and receiver (Inside the application tag) in your manifest like shown below(can also see manifest which is along the code for reference)
//outside the application tag
<uses-permission android:name="com.android.vending.BILLING" />
// Inside the application tag
<service android:name=".BillingService" />
<receiver android:name=".BillingReceiver">
<intent-filter>
<action android:name="com.android.vending.billing.IN_APP_NOTIFY" />
<action android:name="com.android.vending.billing.RESPONSE_CODE" />
<action android:name="com.android.vending.billing.PURCHASE_STATE_CHANGED" />
</intent-filter>
</receiver>
place the following code as mentioned with there places in your activity where purchase is being held.
//at the starting of your onCreate()
startService(new Intent(mContext, BillingService.class));
BillingHelper.setCompletedHandler(mTransactionHandler);
//outside onCreate() Within class
public Handler mTransactionHandler = new Handler(){
public void handleMessage(android.os.Message msg) {
Log.i(TAG, "Transaction complete");
Log.i(TAG, "Transaction status: "+BillingHelper.latestPurchase.purchaseState);
Log.i(TAG, "Item purchased is: "+BillingHelper.latestPurchase.productId);
if(BillingHelper.latestPurchase.isPurchased()){
//code here which is to be performed after successful purchase
}
};
};
//code to initiate a purchase... can be placed in onClickListener etc
if(BillingHelper.isBillingSupported()){
BillingHelper.requestPurchase(mContext, "android.test.purchased");
// where android.test.purchased is test id for fake purchase, when you create products through developer console you can set a code to pass the id(which is given on developer console while creating a product) of the item which is selected for purchase to intiate purchase of that item.
} else {
Log.i(TAG,"Can't purchase on this device");
// Do Anything Heer to show user that purchase not possible on this device
}
Note: To do a test purchase you need to put the public key in BillingSecurity.java as mentioned above secondly you need to upload the apk to the developer console(you can leave it uupublished and unactive) and thirdly you need a real android device(emulator wouldn't work) having updated play store app.
Note: the account needed for in app purchase and described in all above discussion is not just simple publisher account its publisher account embedded with google merchant wallet account. The details can be found in link below.
http://support.google.com/googleplay/android-developer/bin/answer.py?hl=en&answer=113468
1)download
http://developer.android.com/guide/google/play/billing/billing_integrate.html#billing-download
2)add
http://developer.android.com/guide/google/play/billing/billing_integrate.html#billing-add-aidl
3)add permission in your android manifest file
http://developer.android.com/guide/google/play/billing/billing_integrate.html#billing-permission
now your project should look like this...
4) place the public key (you can find it in developer console in the
bottom section of edit profile) in the Security.java in line saying
String base64EncodedPublicKey = "your public key here"
5) and finally your activity which have button should be look like this
public class YourActivity extends Activity implements OnClickListener {
String issueProductId = "Your Product ID";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.updates);
SetInAppBilling();
Button donate = (Button) findViewById(R.id.donate);
donate.setOnClickListener(new Button.OnClickListener() {
public void onClick(View v) {
if (mBillingService.requestPurchase(issueProductId, null)) {
} else {
showDialog(DIALOG_BILLING_NOT_SUPPORTED_ID);
Log.i("tag", "Can't purchase on this device");
}
}
});
}
public void register() {
ResponseHandler.register(mDungeonsPurchaseObserver);
}
public void unregister() {
ResponseHandler.unregister(mDungeonsPurchaseObserver);
}
public void close_unbind() {
if (mPurchaseDatabase != null)
// mPurchaseDatabase.close();
if (mBillingService != null)
mBillingService.unbind();
// stopService(new Intent(this, BillingService.class));
}
/**
* Called when this activity becomes visible.
*/
#Override
protected void onStart() {
super.onStart();
register();
}
/**
* Called when this activity is no longer visible.
*/
#Override
protected void onStop() {
unregister();
super.onStop();
}
#Override
protected void onDestroy() {
close_unbind();
super.onDestroy();
}
private static final String TAG = "YourActivity";
private static final String DB_INITIALIZED = "db_initialized";
// private static final String Dir_Check = "Dir_Check";
private DungeonsPurchaseObserver mDungeonsPurchaseObserver;
private Handler mHandler;
private BillingService mBillingService;
private PurchaseDatabase mPurchaseDatabase;
private static final int DIALOG_CANNOT_CONNECT_ID = 1;
private static final int DIALOG_BILLING_NOT_SUPPORTED_ID = 2;
private Cursor mOwnedItemsCursor;
public void SetInAppBilling() {
mHandler = new Handler();
mDungeonsPurchaseObserver = new DungeonsPurchaseObserver(mHandler);
mBillingService = new BillingService();
mBillingService.setContext(this);
mPurchaseDatabase = new PurchaseDatabase(this);
mOwnedItemsCursor = mPurchaseDatabase
.queryAllPurchasedHistroyTabelItems();
startManagingCursor(mOwnedItemsCursor);
SharedPreferences prefs = getPreferences(MODE_PRIVATE);
boolean initialized = prefs.getBoolean(DB_INITIALIZED, false);
// Check if billing is supported.
ResponseHandler.register(mDungeonsPurchaseObserver);
if (!mBillingService.checkBillingSupported()) {
showDialog(DIALOG_CANNOT_CONNECT_ID);
}
}
private class DungeonsPurchaseObserver extends PurchaseObserver {
public DungeonsPurchaseObserver(Handler handler) {
super(YourActiviy.this, handler);
}
#Override
public void onBillingSupported(boolean supported) {
Log.i(TAG, "supportedCheck: " + supported);
if (Consts.DEBUG) {
Log.i(TAG, "supported: " + supported);
}
if (supported) {
restoreDatabase();
} else {
showDialog(DIALOG_BILLING_NOT_SUPPORTED_ID);
}
}
#Override
public void onPurchaseStateChange(PurchaseState purchaseState,
String itemId, int quantity, long purchaseTime,
String developerPayload) {
if (Consts.DEBUG) {
Log.i(TAG, "onPurchaseStateChange() itemId: " + itemId + " "
+ purchaseState);
}
if (developerPayload == null) {
} else {
}
Log.e(TAG, "onPurchaseStateChangeCheck: " + "onPurchaseStateChange");
if (purchaseState == PurchaseState.PURCHASED) {
/** TODO: */
Toast.makeText(
mContext,
"You successfully upgraded to the entire Volume One. Enjoy!",
Toast.LENGTH_SHORT).show();
finish();
}
}
#Override
public void onRequestPurchaseResponse(RequestPurchase request,
ResponseCode responseCode) {
if (Consts.DEBUG) {
Log.d(TAG, request.mProductId + ": " + responseCode);
}
if (responseCode == ResponseCode.RESULT_OK) {
if (Consts.DEBUG) {
Log.i(TAG, "purchase was successfully sent to server");
}
} else if (responseCode == ResponseCode.RESULT_USER_CANCELED) {
if (Consts.DEBUG) {
Log.i(TAG, "user canceled purchase");
}
} else {
if (Consts.DEBUG) {
Log.i(TAG, "purchase failed");
}
}
}
#Override
public void onRestoreTransactionsResponse(RestoreTransactions request,
ResponseCode responseCode) {
if (responseCode == ResponseCode.RESULT_OK) {
if (Consts.DEBUG) {
Log.d(TAG, "completed RestoreTransactions request");
}
// Update the shared preferences so that we don't perform
// a RestoreTransactions again.
SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor edit = prefs.edit();
edit.putBoolean(DB_INITIALIZED, true);
edit.commit();
mOwnedItemsCursor = mPurchaseDatabase
.queryAllPurchasedHistroyTabelItems();
Log.d(TAG, String.valueOf(mOwnedItemsCursor.getCount()));
startManagingCursor(mOwnedItemsCursor);
if (mOwnedItemsCursor.getCount() > 0) {
Log.d(TAG, "Updating the DB");
Toast.makeText(
mContext,
"You successfully upgraded to the entire Volume One. Enjoy!",
Toast.LENGTH_SHORT).show();
finish();
}
} else {
if (Consts.DEBUG) {
Log.d(TAG, "RestoreTransactions error: " + responseCode);
}
}
}
}
#Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_CANNOT_CONNECT_ID:
return createDialog(R.string.cannot_connect_title,
R.string.cannot_connect_message);
case DIALOG_BILLING_NOT_SUPPORTED_ID:
return createDialog(R.string.billing_not_supported_title,
R.string.billing_not_supported_message);
default:
return null;
}
}
private Dialog createDialog(int titleId, int messageId) {
String helpUrl = replaceLanguageAndRegion(getString(R.string.help_url));
if (Consts.DEBUG) {
Log.i(TAG, helpUrl);
}
final Uri helpUri = Uri.parse(helpUrl);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(titleId)
.setIcon(android.R.drawable.stat_sys_warning)
.setMessage(messageId)
.setCancelable(false)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(R.string.learn_more,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int which) {
Intent intent = new Intent(Intent.ACTION_VIEW,
helpUri);
startActivity(intent);
}
});
return builder.create();
}
/**
* Replaces the language and/or country of the device into the given string.
* The pattern "%lang%" will be replaced by the device's language code and
* the pattern "%region%" will be replaced with the device's country code.
*
* #param str
* the string to replace the language/country within
* #return a string containing the local language and region codes
*/
private String replaceLanguageAndRegion(String str) {
// Substitute language and or region if present in string
if (str.contains("%lang%") || str.contains("%region%")) {
Locale locale = Locale.getDefault();
str = str.replace("%lang%", locale.getLanguage().toLowerCase());
str = str.replace("%region%", locale.getCountry().toLowerCase());
}
return str;
}
private void restoreDatabase() {
SharedPreferences prefs = getPreferences(MODE_PRIVATE);
boolean initialized = prefs.getBoolean(DB_INITIALIZED, false);
if (!initialized) {
mBillingService.restoreTransactions();
// Toast.makeText(this, "restoring...", Toast.LENGTH_LONG).show();
}
}
}
There are 2 ways
Maintain a database on your server end, and create a table for user+purchased list of products+expiry date
Or the client app to save a encrypted code on shared preferences (so that it cant be hacked easily)
Android API's dont provide you with any inventory apis to maintain your purchases.
I used the 2nd option.
-- single product purchase
store.purchase( { "android.test.purchased" } )
-- multi-item purchase
store.purchase( { "android.test.purchased", "android.test.canceled" } )
in reference to Getting Started with Android In-app Billing
this code maybe the one you are looking for
The sample in-app billing code from Google is a good start. I have posted a link to a tutorial I gave which is three parts. This link is the first of three. I hope it helps.
http://www.mobileoped.com/2012/04/06/android-google-play-in-app-billing-part-1/
I've looked at a number of other threads with similar titles, and none seem to cover my problem. So, here goes.
I'm using the Google market expansion files (apkx) library and sample code, with a few modifications. This code relies on receiving callbacks from a service which handles background downloading, licence checks etc.
I have a bug where the service doesn't get correctly attached, which results in a softlock. To make this more unhelpful, this bug never happens on some devices, but occurs about two thirds of the time on other devices. I believe it to be independent of Android version, certainly I have two devices running 2.3.4, one of which (a Nexus S) doesn't have the problem, the other (an HTC Evo 3D) does.
To attempt to connect to the service, bindService is called and returns true. OnBind then gets called as expected and returns a sensible value but (when the bug occurs) onServiceConnected doesn't happen (I've waited 20 minutes just in case).
Has anyone else seen anything like this? If not, any guesses for what I might have done to cause such behaviour? If no-one has any thoughts, I'll post some code tomorrow.
EDIT: Here's the relevant code. If I've missed anything, please ask.
Whilst adding this code, I found a minor bug. Fixing it caused the frequency of the problem I'm trying to solve to change from 2 times in 3 to about 1 time in 6 on the phone I'm testing it on; no idea about effects on other phones. This continues to suggest to me a race condition or similar, but I've no idea what with.
OurDownloaderActivity.java (copied and changed from Google sample code)
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
//Test the licence is up to date
//if (current stored licence has expired)
{
startLicenceCheck();
initializeDownloadUI();
return;
}
...
}
#Override
protected void onResume() {
if (null != mDownloaderClientStub) {
mDownloaderClientStub.connect(this);
}
super.onResume();
}
private void startLicenceCheck()
{
Intent launchIntent = OurDownloaderActivity.this
.getIntent();
Intent intentToLaunchThisActivityFromNotification = new Intent(OurDownloaderActivity
.this, OurDownloaderActivity.this.getClass());
intentToLaunchThisActivityFromNotification.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_CLEAR_TOP);
intentToLaunchThisActivityFromNotification.setAction(launchIntent.getAction());
if (launchIntent.getCategories() != null) {
for (String category : launchIntent.getCategories()) {
intentToLaunchThisActivityFromNotification.addCategory(category);
}
}
// Build PendingIntent used to open this activity from Notification
PendingIntent pendingIntent = PendingIntent.getActivity(OurDownloaderActivity.this,
0, intentToLaunchThisActivityFromNotification,
PendingIntent.FLAG_UPDATE_CURRENT);
DownloaderService.startLicenceCheck(this, pendingIntent, OurDownloaderService.class);
}
initializeDownloadUI()
{
mDownloaderClientStub = DownloaderClientMarshaller.CreateStub
(this, OurDownloaderService.class);
//do a load of UI setup
...
}
//This should be called by the Stub's onServiceConnected method
/**
* Critical implementation detail. In onServiceConnected we create the
* remote service and marshaler. This is how we pass the client information
* back to the service so the client can be properly notified of changes. We
* must do this every time we reconnect to the service.
*/
#Override
public void onServiceConnected(Messenger m) {
mRemoteService = DownloaderServiceMarshaller.CreateProxy(m);
mRemoteService.onClientUpdated(mDownloaderClientStub.getMessenger());
}
DownloaderService.java (in Google market expansion library but somewhat edited )
//this is the onBind call that happens fine; the value it returns is definitely not null
#Override
public IBinder onBind(Intent paramIntent) {
return this.mServiceMessenger.getBinder();
}
final private IStub mServiceStub = DownloaderServiceMarshaller.CreateStub(this);
final private Messenger mServiceMessenger = mServiceStub.getMessenger();
//MY CODE, derived from Google's code
//I have seen the bug occur with a service started by Google's code too,
//but this code happens more often so is more repeatably related to the problem
public static void startLicenceCheck(Context context, PendingIntent pendingIntent, Class<?> serviceClass)
{
String packageName = serviceClass.getPackage().getName();
String className = serviceClass.getName();
Intent fileIntent = new Intent();
fileIntent.setClassName(packageName, className);
fileIntent.putExtra(EXTRA_LICENCE_EXPIRED, true);
fileIntent.putExtra(EXTRA_PENDING_INTENT, pendingIntent);
context.startService(fileIntent);
}
#Override
protected void onHandleIntent(Intent intent) {
setServiceRunning(true);
try {
final PendingIntent pendingIntent = (PendingIntent) intent
.getParcelableExtra(EXTRA_PENDING_INTENT);
if (null != pendingIntent)
{
mNotification.setClientIntent(pendingIntent);
mPendingIntent = pendingIntent;
} else if (null != mPendingIntent) {
mNotification.setClientIntent(mPendingIntent);
} else {
Log.e(LOG_TAG, "Downloader started in bad state without notification intent.");
return;
}
if(intent.getBooleanExtra(EXTRA_LICENCE_EXPIRED, false))
{
//we are here due to startLicenceCheck
updateExpiredLVL(this);
return;
}
...
}
}
//MY CODE, based on Google's, again
public void updateExpiredLVL(final Context context) {
Context c = context.getApplicationContext();
Handler h = new Handler(c.getMainLooper());
h.post(new LVLExpiredUpdateRunnable(c));
}
private class LVLExpiredUpdateRunnable implements Runnable
{
LVLExpiredUpdateRunnable(Context context) {
mContext = context;
}
final Context mContext;
#Override
public void run() {
setServiceRunning(true);
mNotification.onDownloadStateChanged(IDownloaderClient.STATE_LVL_UPDATING);
String deviceId = getDeviceId(mContext);
final APKExpansionPolicy aep = new APKExpansionPolicy(mContext,
new AESObfuscator(getSALT(), mContext.getPackageName(), deviceId));
// Construct the LicenseChecker with a Policy.
final LicenseChecker checker = new LicenseChecker(mContext, aep,
getPublicKey() // Your public licensing key.
);
checker.checkAccess(new LicenseCheckerCallback() {
...
});
}
}
DownloaderClientMarshaller.java (in Google market expansion library)
public static IStub CreateStub(IDownloaderClient itf, Class<?> downloaderService) {
return new Stub(itf, downloaderService);
}
and the Stub class from the same file:
private static class Stub implements IStub {
private IDownloaderClient mItf = null;
private Class<?> mDownloaderServiceClass;
private boolean mBound;
private Messenger mServiceMessenger;
private Context mContext;
/**
* Target we publish for clients to send messages to IncomingHandler.
*/
final Messenger mMessenger = new Messenger(new Handler() {
#Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_ONDOWNLOADPROGRESS:
Bundle bun = msg.getData();
if ( null != mContext ) {
bun.setClassLoader(mContext.getClassLoader());
DownloadProgressInfo dpi = (DownloadProgressInfo) msg.getData()
.getParcelable(PARAM_PROGRESS);
mItf.onDownloadProgress(dpi);
}
break;
case MSG_ONDOWNLOADSTATE_CHANGED:
mItf.onDownloadStateChanged(msg.getData().getInt(PARAM_NEW_STATE));
break;
case MSG_ONSERVICECONNECTED:
mItf.onServiceConnected(
(Messenger) msg.getData().getParcelable(PARAM_MESSENGER));
break;
}
}
});
public Stub(IDownloaderClient itf, Class<?> downloaderService) {
mItf = itf;
mDownloaderServiceClass = downloaderService;
}
/**
* Class for interacting with the main interface of the service.
*/
private ServiceConnection mConnection = new ServiceConnection() {
//this is the critical call that never happens
public void onServiceConnected(ComponentName className, IBinder service) {
// This is called when the connection with the service has been
// established, giving us the object we can use to
// interact with the service. We are communicating with the
// service using a Messenger, so here we get a client-side
// representation of that from the raw IBinder object.
mServiceMessenger = new Messenger(service);
mItf.onServiceConnected(
mServiceMessenger);
mBound = true;
}
public void onServiceDisconnected(ComponentName className) {
// This is called when the connection with the service has been
// unexpectedly disconnected -- that is, its process crashed.
mServiceMessenger = null;
mBound = false;
}
};
#Override
public void connect(Context c) {
mContext = c;
Intent bindIntent = new Intent(c, mDownloaderServiceClass);
bindIntent.putExtra(PARAM_MESSENGER, mMessenger);
if ( !c.bindService(bindIntent, mConnection, 0) ) {
if ( Constants.LOGVV ) {
Log.d(Constants.TAG, "Service Unbound");
}
}
}
#Override
public void disconnect(Context c) {
if (mBound) {
c.unbindService(mConnection);
mBound = false;
}
mContext = null;
}
#Override
public Messenger getMessenger() {
return mMessenger;
}
}
DownloaderServiceMarshaller.java (in Google market expansion library, unchanged)
private static class Proxy implements IDownloaderService {
private Messenger mMsg;
private void send(int method, Bundle params) {
Message m = Message.obtain(null, method);
m.setData(params);
try {
mMsg.send(m);
} catch (RemoteException e) {
e.printStackTrace();
}
}
public Proxy(Messenger msg) {
mMsg = msg;
}
#Override
public void requestAbortDownload() {
send(MSG_REQUEST_ABORT_DOWNLOAD, new Bundle());
}
#Override
public void requestPauseDownload() {
send(MSG_REQUEST_PAUSE_DOWNLOAD, new Bundle());
}
#Override
public void setDownloadFlags(int flags) {
Bundle params = new Bundle();
params.putInt(PARAMS_FLAGS, flags);
send(MSG_SET_DOWNLOAD_FLAGS, params);
}
#Override
public void requestContinueDownload() {
send(MSG_REQUEST_CONTINUE_DOWNLOAD, new Bundle());
}
#Override
public void requestDownloadStatus() {
send(MSG_REQUEST_DOWNLOAD_STATE, new Bundle());
}
#Override
public void onClientUpdated(Messenger clientMessenger) {
Bundle bundle = new Bundle(1);
bundle.putParcelable(PARAM_MESSENGER, clientMessenger);
send(MSG_REQUEST_CLIENT_UPDATE, bundle);
}
}
private static class Stub implements IStub {
private IDownloaderService mItf = null;
final Messenger mMessenger = new Messenger(new Handler() {
#Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_REQUEST_ABORT_DOWNLOAD:
mItf.requestAbortDownload();
break;
case MSG_REQUEST_CONTINUE_DOWNLOAD:
mItf.requestContinueDownload();
break;
case MSG_REQUEST_PAUSE_DOWNLOAD:
mItf.requestPauseDownload();
break;
case MSG_SET_DOWNLOAD_FLAGS:
mItf.setDownloadFlags(msg.getData().getInt(PARAMS_FLAGS));
break;
case MSG_REQUEST_DOWNLOAD_STATE:
mItf.requestDownloadStatus();
break;
case MSG_REQUEST_CLIENT_UPDATE:
mItf.onClientUpdated((Messenger) msg.getData().getParcelable(
PARAM_MESSENGER));
break;
}
}
});
public Stub(IDownloaderService itf) {
mItf = itf;
}
#Override
public Messenger getMessenger() {
return mMessenger;
}
#Override
public void connect(Context c) {
}
#Override
public void disconnect(Context c) {
}
}
/**
* Returns a proxy that will marshall calls to IDownloaderService methods
*
* #param ctx
* #return
*/
public static IDownloaderService CreateProxy(Messenger msg) {
return new Proxy(msg);
}
/**
* Returns a stub object that, when connected, will listen for marshalled
* IDownloaderService methods and translate them into calls to the supplied
* interface.
*
* #param itf An implementation of IDownloaderService that will be called
* when remote method calls are unmarshalled.
* #return
*/
public static IStub CreateStub(IDownloaderService itf) {
return new Stub(itf);
}