What happen if don't call stop() function in Android Google Analytics? - android

I've been looking through the code of the GoogleIO Android app and I notice the their did not call stop() function on the GoogleAnalytics' instance. What will happen if we don't call stop()?
This is the code:
package com.google.android.apps.iosched.util;
import com.google.android.apps.analytics.GoogleAnalyticsTracker;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Build;
import android.preference.PreferenceManager;
import android.util.Log;
/**
* Helper singleton class for the Google Analytics tracking library.
*/
public class AnalyticsUtils {
private static final String TAG = "AnalyticsUtils";
GoogleAnalyticsTracker mTracker;
private Context mApplicationContext;
/**
* The analytics tracking code for the app.
*/
// TODO: insert your Analytics UA code here.
private static final String UACODE = "INSERT_YOUR_ANALYTICS_UA_CODE_HERE";
private static final int VISITOR_SCOPE = 1;
private static final String FIRST_RUN_KEY = "firstRun";
private static final boolean ANALYTICS_ENABLED = true;
private static AnalyticsUtils sInstance;
/**
* Returns the global {#link AnalyticsUtils} singleton object, creating one if necessary.
*/
public static AnalyticsUtils getInstance(Context context) {
if (!ANALYTICS_ENABLED) {
return sEmptyAnalyticsUtils;
}
if (sInstance == null) {
if (context == null) {
return sEmptyAnalyticsUtils;
}
sInstance = new AnalyticsUtils(context);
}
return sInstance;
}
private AnalyticsUtils(Context context) {
if (context == null) {
// This should only occur for the empty Analytics utils object.
return;
}
mApplicationContext = context.getApplicationContext();
mTracker = GoogleAnalyticsTracker.getInstance();
// Unfortunately this needs to be synchronous.
mTracker.start(UACODE, 300, mApplicationContext);
Log.d(TAG, "Initializing Analytics");
// Since visitor CV's should only be declared the first time an app runs, check if
// it's run before. Add as necessary.
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mApplicationContext);
final boolean firstRun = prefs.getBoolean(FIRST_RUN_KEY, true);
if (firstRun) {
Log.d(TAG, "Analytics firstRun");
String apiLevel = Integer.toString(Build.VERSION.SDK_INT);
String model = Build.MODEL;
mTracker.setCustomVar(1, "apiLevel", apiLevel, VISITOR_SCOPE);
mTracker.setCustomVar(2, "model", model, VISITOR_SCOPE);
// Close out so we never run this block again, unless app is removed & =
// reinstalled.
prefs.edit().putBoolean(FIRST_RUN_KEY, false).commit();
}
}
public void trackEvent(final String category, final String action, final String label,
final int value) {
// We wrap the call in an AsyncTask since the Google Analytics library writes to disk
// on its calling thread.
new AsyncTask<Void, Void, Void>() {
#Override
protected Void doInBackground(Void... voids) {
try {
mTracker.trackEvent(category, action, label, value);
Log.d(TAG, "iosched Analytics trackEvent: "
+ category + " / " + action + " / " + label + " / " + value);
} catch (Exception e) {
// We don't want to crash if there's an Analytics library exception.
Log.w(TAG, "iosched Analytics trackEvent error: "
+ category + " / " + action + " / " + label + " / " + value, e);
}
return null;
}
}.execute();
}
public void trackPageView(final String path) {
// We wrap the call in an AsyncTask since the Google Analytics library writes to disk
// on its calling thread.
new AsyncTask<Void, Void, Void>() {
#Override
protected Void doInBackground(Void... voids) {
try {
mTracker.trackPageView(path);
Log.d(TAG, "iosched Analytics trackPageView: " + path);
} catch (Exception e) {
// We don't want to crash if there's an Analytics library exception.
Log.w(TAG, "iosched Analytics trackPageView error: " + path, e);
}
return null;
}
}.execute();
}
/**
* Empty instance for use when Analytics is disabled or there was no Context available.
*/
private static AnalyticsUtils sEmptyAnalyticsUtils = new AnalyticsUtils(null) {
#Override
public void trackEvent(String category, String action, String label, int value) {}
#Override
public void trackPageView(String path) {}
};
}
As you can see, they start the tracker with 5 minutes interval mTracker.start(UACODE, 300, mApplicationContext); but never call the mTracker.stop() method. Will there be any consequences? Does it mean the service will dispatch the data even when the app is closed or stopped?

EasyTracker - I found this blog entry in the google analytics blog. There they describe an EasyTracker class, that wraps the normal Tracker class and has some nice features.
Configuration via resource file (no coding needed)
Everything's done in a separate thread (not in the ui-thread as with the normal Tracker)
...
And this EasyTracker does not need to be stopped explicity either.

There shouldn't be happen much -- when you close or stop the app, and even if it still lives in the background and there's no activity anymore, no new data will be gathered (because no one invokes track() anymore. So I think it's just good manners to explicitly stop the tracker.

Related

RxJava- RxAndroid form validation on dynamic EditText

I have form that can have variable number of EditText that needs to be validated before form submission. I can perform validation check if EditTexts are fixed in number like following -
Observable<CharSequence> emailObservable = RxTextView.textChanges(editEmail).skip(1);
Observable<CharSequence> passwordObservable = RxTextView.textChanges(editPassword).skip(1);
mFormValidationSubscription = Observable.combineLatest(emailObservable, passwordObservable,
(newEmail, newPassword) -> {
boolean emailValid = !TextUtils.isEmpty(newEmail) && android.util.Patterns.EMAIL_ADDRESS.matcher(newEmail).matches();
if(!emailValid) {
emailInputLayout.setError(getString(R.string.error_invalid_email));
emailInputLayout.setErrorEnabled(true);
}else {
emailInputLayout.setError(null);
emailInputLayout.setErrorEnabled(false);
}
boolean passValid = !TextUtils.isEmpty(newPassword) && newPassword.length() > 4;
if (!passValid) {
passwordInputLayout.setError(getString(R.string.error_invalid_password));
passwordInputLayout.setErrorEnabled(true);
} else {
passwordInputLayout.setError(null);
passwordInputLayout.setErrorEnabled(true);
}
return emailValid && passValid;
}).subscribe(isValid ->{
mSubmitButton.setEnabled(isValid);
});
But now as there are variable number of inputs I tried creating a list of Observable<CharSequence> and Observable.combineLatest() but I'm stuck as to proceed with that.
List<Observable<CharSequence>> observableList = new ArrayList<>();
for(InputRule inputRule : mMaterial.getRules()) {
View vInputRow = inflater.inflate(R.layout.item_material_input_row, null, false);
StyledEditText styledEditText = ((StyledEditText)vInputRow.findViewById(R.id.edit_input));
styledEditText.setHint(inputRule.getName());
Observable<CharSequence> observable = RxTextView.textChanges(styledEditText).skip(1);
observableList.add(observable);
linearLayout.addView(vInputRow);
}
Observable.combineLatest(observableList,......); // What should go in place of these "......"
How can I perform checks for a valid charsequence for each input field. I looked into flatMap(), map(), filter() methods but I don't know how to use them.
Yes, you process abitrary number of Observables in .combineLatest(), but there is still workaround. I got interested in this problem and came up with following solution- we can store information about some data source - last value and source ID (String and resource id) and tunnell all data into some common pipe. For that we can use PublishSubject. We also need to track connection state, for that we should save Subscription to each source on subscription and sever it when we unsubscribe from that source.
We store last data from each source, so we can tell user what source just emitted new value, callback will only contain source id. User can get last value of any source by source id.
I came up with the following code:
import android.util.Log;
import android.widget.EditText;
import com.jakewharton.rxbinding.widget.RxTextView;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import rx.Observable;
import rx.Subscription;
import rx.functions.Action1;
import rx.subjects.PublishSubject;
public class MultiSourceCombinator {
String LOG_TAG = MultiSourceCombinator.class.getSimpleName();
/**
* We can't handle arbitrary number of sources by CombineLatest, but we can pass data along
* with information about source (sourceId)
*/
private static class SourceData{
String data = "";
Integer sourceId = 0;
}
/**
* Keep id of source, subscription to that source and last value emitted
* by source. This value is passed when source is attached
*/
private class SourceInfo{
Subscription sourceTracking;
Integer sourceId;
SourceData lastData;
SourceInfo(int sourceId, String data){
this.sourceId = sourceId;
// initialize last data with empty value
SourceData d = new SourceData();
d.data = data;
d.sourceId = sourceId;
this.lastData = d;
}
}
/**
* We can tunnel data from all sources into single pipe. Subscriber can treat it as
* Observable<SourceData>
*/
private PublishSubject<SourceData> dataDrain;
/**
* Stores all sources by their ids.
*/
Map<Integer, SourceInfo> sources;
/**
* Callback, notified whenever source emit new data. it receives source id.
* When notification is received by client, it can get value from source by using
* getLastSourceValue(sourceId) method
*/
Action1<Integer> sourceUpdateCallback;
public MultiSourceCombinator(){
dataDrain = PublishSubject.create();
sources = new HashMap<>();
sourceUpdateCallback = null;
// We have to process data, ccoming from common pipe
dataDrain.asObservable()
.subscribe(newValue -> {
if (sourceUpdateCallback == null) {
Log.w(LOG_TAG, "Source " + newValue.sourceId + "emitted new value, " +
"but used did't set callback ");
} else {
sourceUpdateCallback.call(newValue.sourceId);
}
});
}
/**
* Disconnect from all sources (sever Connection (s))
*/
public void stop(){
Log.i(LOG_TAG, "Unsubscribing from all sources");
// copy references to aboid ConcurrentModificatioinException
ArrayList<SourceInfo> t = new ArrayList(sources.values());
for (SourceInfo si : t){
removeSource(si.sourceId);
}
// right now there must be no active sources
if (!sources.isEmpty()){
throw new RuntimeException("There must be no active sources");
}
}
/**
* Create new source from edit field, subscribe to this source and save subscription for
* further tracking.
* #param editText
*/
public void addSource(EditText editText, int sourceId){
if (sources.containsKey(sourceId)){
Log.e(LOG_TAG, "Source with id " + sourceId + " already exist");
return;
}
Observable<CharSequence> source = RxTextView.textChanges(editText).skip(1);
String lastValue = editText.getText().toString();
Log.i(LOG_TAG, "Source with id " + sourceId + " has data " + lastValue);
// Redirect data coming from source to common pipe, to do that attach source id to
// data string
Subscription sourceSubscription = source.subscribe(text -> {
String s = new String(text.toString());
SourceData nextValue = new SourceData();
nextValue.sourceId = sourceId;
nextValue.data = s;
Log.i(LOG_TAG, "Source " + sourceId + "emits new value: " + s);
// save vlast value
sources.get(sourceId).lastData.data = s;
// pass new value down pipeline
dataDrain.onNext(nextValue);
});
// create SourceInfo
SourceInfo sourceInfo = new SourceInfo(sourceId, lastValue);
sourceInfo.sourceTracking = sourceSubscription;
sources.put(sourceId, sourceInfo);
}
/**
* Unsubscribe source from common pipe and remove it from list of sources
* #param sourceId
* #throws IllegalArgumentException
*/
public void removeSource(Integer sourceId) throws IllegalArgumentException {
if (!sources.containsKey(sourceId)){
throw new IllegalArgumentException("There is no source with id: " + sourceId);
}
SourceInfo si = sources.get(sourceId);
Subscription s = si.sourceTracking;
if (null != s && !s.isUnsubscribed()){
Log.i(LOG_TAG, "source " + sourceId + " is active, unsubscribing from it");
si.sourceTracking.unsubscribe();
si.sourceTracking = null;
}
// source is disabled, remove it from list
Log.i(LOG_TAG, "Source " + sourceId + " is disabled ");
sources.remove(sourceId);
}
/**
* User can get value from any source by using source ID.
* #param sourceId
* #return
* #throws IllegalArgumentException
*/
public String getLastSourceValue(Integer sourceId) throws IllegalArgumentException{
if (!sources.containsKey(sourceId)){
throw new IllegalArgumentException("There is no source with id: " + sourceId);
}
String lastValue = sources.get(sourceId).lastData.data;
return lastValue;
}
public void setSourceUpdateCallback(Action1<Integer> sourceUpdateFeedback) {
this.sourceUpdateCallback = sourceUpdateFeedback;
}
}
And we can use it in UI like this:
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import butterknife.BindView;
import butterknife.ButterKnife;
public class EdiTextTestActivity extends Activity {
#BindView(R.id.aet_et1)
public EditText et1;
#BindView(R.id.aet_et2)
public EditText et2;
#BindView(R.id.aet_et3)
public EditText et3;
private MultiSourceCombinator multiSourceCombinator;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit_text_test);
ButterKnife.bind(this);
multiSourceCombinator = new MultiSourceCombinator();
multiSourceCombinator.setSourceUpdateCallback(id -> {
Toast.makeText(EdiTextTestActivity.this, "New value from source: " + id + " : " +
multiSourceCombinator.getLastSourceValue(id), Toast.LENGTH_SHORT).show();
});
}
#Override
protected void onPause() {
// stop tracking all fields
multiSourceCombinator.stop();
super.onPause();
}
#Override
protected void onResume() {
super.onResume();
// Register fields
multiSourceCombinator.addSource(et1, R.id.aet_et1);
multiSourceCombinator.addSource(et2, R.id.aet_et2);
multiSourceCombinator.addSource(et3, R.id.aet_et3);
}
}
I have a solution for you without using lambda expressions (as I could not compile it with lambdas).
Use the same operator as you wanted:
public static <T, R> Observable<R> combineLatest(List<? extends Observable<? extends T>> sources, FuncN<? extends R> combineFunction)
Observable.combineLatest(observableList, new FuncN<Boolean>() {
#Override
public Boolean call(Object... objects) {
boolean isValid = true;
CharSequence input;
for (int i = 0; i < objects.length; i++) {
input = (CharSequence) objects[i];
switch (i) {
case 1:
//First text field value
break;
case 2:
//Second text field value
break;
default:
isValid = false;
}
}
return isValid;
}
})
The reason lambda expressions don't work is probably in second parameter of the function combineLatest(...):
public interface FuncN<R> extends Function {
R call(Object... args);
}
According to this post implementing Arbitrary Number of Arguments is hard to do and workarounds need to be created.
RxJava v2 is compatible with Java 8 and has a different implementation of combineLatest
I used R. Zagórski's answer as guide on how to do this with Kotlin
This is what worked for me in the end.
val ob1 = RxTextView.textChanges(field1).skip(1)
val ob2 = RxTextView.textChanges(field2).skip(1)
val ob3 = RxTextView.textChanges(field3).skip(1)
val observableList = arrayListOf<Observable<CharSequence>>()
observableList.add(ob1)
observableList.add(ob3)
val formValidator = Observable.combineLatest(observableList, {
var isValid = true
it.forEach {
val string = it.toString()
if (string.isEmpty()) {
isValid = false
}
}
return#combineLatest isValid
})
formValidator.subscribe { isValid ->
if (isValid) {
//do something
} else {
//do something
}
}

Not able to get tap points from tapjoy offerwall?

I have developed an android app. In this app I have integrated tapjoy and everbadge offerwalls. In tapjoy offerwall, I am calling following method to show offerwall.
TapjoyConnect.getTapjoyConnectInstance().showOffers();
but when I complete an offer I am not able to see tap points in toast. following is the code I have written for tapjoy offerwall.
public class TapjoyOfferwall implements TapjoyEarnedPointsNotifier, TapjoyNotifier{
private Activity context;
//private AppPreferences appPreferences;
private String AppId_TapJoy = "bba49f11-b87f-4c0f-9632-21aa810dd6f1";
private String SecretKey_TapJoy = "yiQIURFEeKm0zbOggubu";
private String displayText = "";
boolean update_text = false;
boolean earnedPoints = false;
int point_total;
String currency_name;
public TapjoyOfferwall(Activity context) {
super();
this.context = context;
//appPreferences = new AppPreferences(context);
// Enables logging to the console.
TapjoyLog.enableLogging(true);
// OPTIONAL: For custom startup flags.
Hashtable<String, String> flags = new Hashtable<String, String>();
flags.put(TapjoyConnectFlag.ENABLE_LOGGING, "true");
// Connect with the Tapjoy server. Call this when the application first starts.
// REPLACE THE APP ID WITH YOUR TAPJOY APP ID.
// REPLACE THE SECRET KEY WITH YOUR SECRET KEY.
TapjoyConnect.requestTapjoyConnect(context,AppId_TapJoy, SecretKey_TapJoy,flags);
//TapjoyConnect.getTapjoyConnectInstance().setUserID(""+appPreferences.GetUserID());
// Set our earned points notifier to this class.
TapjoyConnect.getTapjoyConnectInstance().setEarnedPointsNotifier(this);
//LaunchTapJoyOfferwall();
}
#Override
public void earnedTapPoints(int amount)
{
earnedPoints = true;
update_text = true;
displayText = "You've just earned " + amount + " Tap Points!";
// We must use a handler since we cannot update UI elements from a different thread.
System.out.println("displayText :: "+displayText);
OEPUtil.showAppToast(context, displayText);
}
public void LaunchTapJoyOfferwall() {
TapjoyConnect.getTapjoyConnectInstance().showOffers();
}
#SuppressWarnings("deprecation")
public void SetBannerAutoRefresh() {
TapjoyConnect.getTapjoyConnectInstance().enableBannerAdAutoRefresh(false);
}
public void SetShutDown() {
TapjoyConnect.getTapjoyConnectInstance().sendShutDownEvent();
}
}
this class I am accessing in one of my activity class on click of a button.
Please let me know whatever I am doing is correct or not and how can I get points??
This might help you..
http://www.jjask.com/32814/android-tapjoy-offerwall-never-makes-callback
basically
#Override
protected void onResume()
{
TapjoyConnect.getTapjoyConnectInstance().getTapPoints(this);
super.onResume();
}

Android consuming RestService with/without cache

I am currently in the process of creating a high performance mobile application. Now i am looking at various design patterns for consuming rest services. One such pattern that stands out is the Google IO discussion here. How i have am looking at the code to develop this design. I will be using Spring Rest for doing the actual HTTP Rest and serialization to POJO with the Serialization Library. I came across this implementation here, and will be using it as a blue print for my application. Now a major question is here.
public interface HttpMethods {
public Object getForObject(Object ... params);
public Object putForObject(Object ... params);
}
public class LocationsHttpMethods implements HttpMethods{
private final Context mContext;
public LocationsHttpMethods(Context context)
{
mContext=context;
}
#Override
public Location[] getForObject(Object... params) {
return null;
}
#Override
public Object putForObject(Object... params) {
return null;
}
}
My Location is just a pojo class. Now the question that troubles me is that the second link that i have given just uses Boolean to return data. I will be returning an array of something.
package com.confiz.rest.services;
import java.util.ArrayList;
import java.util.HashMap;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import com.confiz.rest.providers.IProvider;
import com.confiz.rest.providers.LocationsProvider;
public class ProcessorService extends Service
{
private Integer lastStartId;
private final Context mContext = this;
/**
* The keys to be used for the required actions to start this service.
*/
public static class Extras
{
/**
* The provider which the called method is on.
*/
public static final String PROVIDER_EXTRA = "PROVIDER_EXTRA";
/**
* The method to call.
*/
public static final String METHOD_EXTRA = "METHOD_EXTRA";
/**
* The action to used for the result intent.
*/
public static final String RESULT_ACTION_EXTRA = "RESULT_ACTION_EXTRA";
/**
* The extra used in the result intent to return the result.
*/
public static final String RESULT_EXTRA = "RESULT_EXTRA";
}
private final HashMap<String, AsyncServiceTask> mTasks = new HashMap<String, AsyncServiceTask>();
/**
* Identifier for each supported provider.
* Cannot use 0 as Bundle.getInt(key) returns 0 when the key does not exist.
*/
public static class Providers
{
public static final int LOATIONS_PROVIDER = 1;
}
private IProvider GetProvider(int providerId)
{
switch(providerId)
{
case Providers.LOATIONS_PROVIDER:
return new LocationsProvider(this);
}
return null;
}
/**
* Builds a string identifier for this method call.
* The identifier will contain data about:
* What processor was the method called on
* What method was called
* What parameters were passed
* This should be enough data to identify a task to detect if a similar task is already running.
*/
private String getTaskIdentifier(Bundle extras)
{
String[] keys = extras.keySet().toArray(new String[0]);
java.util.Arrays.sort(keys);
StringBuilder identifier = new StringBuilder();
for (int keyIndex = 0; keyIndex < keys.length; keyIndex++)
{
String key = keys[keyIndex];
// The result action may be different for each call.
if (key.equals(Extras.RESULT_ACTION_EXTRA))
{
continue;
}
identifier.append("{");
identifier.append(key);
identifier.append(":");
identifier.append(extras.get(key).toString());
identifier.append("}");
}
return identifier.toString();
}
#Override
public int onStartCommand(Intent intent, int flags, int startId)
{
// This must be synchronised so that service is not stopped while a new task is being added.
synchronized (mTasks)
{
// stopSelf will be called later and if a new task is being added we do not want to stop the service.
lastStartId = startId;
Bundle extras = intent.getExtras();
String taskIdentifier = getTaskIdentifier(extras);
Log.i("ProcessorService", "starting " + taskIdentifier);
// If a similar task is already running then lets use that task.
AsyncServiceTask task = mTasks.get(taskIdentifier);
if (task == null)
{
task = new AsyncServiceTask(taskIdentifier, extras);
mTasks.put(taskIdentifier, task);
// AsyncTasks are by default only run in serial (depending on the android version)
// see android documentation for AsyncTask.execute()
task.execute((Void[]) null);
}
// Add this Result Action to the task so that the calling activity can be notified when the task is complete.
String resultAction = extras.getString(Extras.RESULT_ACTION_EXTRA);
if (resultAction != "")
{
task.addResultAction(extras.getString(Extras.RESULT_ACTION_EXTRA));
}
}
return START_STICKY;
}
#Override
public IBinder onBind(Intent intent)
{
return null;
}
public class AsyncServiceTask extends AsyncTask<Void, Void, Object>
{
private final Bundle mExtras;
private final ArrayList<String> mResultActions = new ArrayList<String>();
private final String mTaskIdentifier;
/**
* Constructor for AsyncServiceTask
*
* #param taskIdentifier A string which describes the method being called.
* #param extras The Extras from the Intent which was used to start this method call.
*/
public AsyncServiceTask(String taskIdentifier, Bundle extras)
{
mTaskIdentifier = taskIdentifier;
mExtras = extras;
}
public void addResultAction(String resultAction)
{
if (!mResultActions.contains(resultAction))
{
mResultActions.add(resultAction);
}
}
#Override
protected Object doInBackground(Void... params)
{
Log.i("ProcessorService", "working " + mTaskIdentifier);
Object result = false;
final int providerId = mExtras.getInt(Extras.PROVIDER_EXTRA);
final int methodId = mExtras.getInt(Extras.METHOD_EXTRA);
if (providerId != 0 && methodId != 0)
{
final IProvider provider = GetProvider(providerId);
if (provider != null)
{
try
{
result = provider.RunTask(methodId, mExtras);
} catch (Exception e)
{
result = false;
}
}
}
return result;
}
#Override
protected void onPostExecute(Object result)
{
// This must be synchronised so that service is not stopped while a new task is being added.
synchronized (mTasks)
{
Log.i("ProcessorService", "finishing " + mTaskIdentifier);
// Notify the caller(s) that the method has finished executing
for (int i = 0; i < mResultActions.size(); i++)
{
Intent resultIntent = new Intent(mResultActions.get(i));
//What to do here
resultIntent.put(Extras.RESULT_EXTRA, true);
//What to do here ends.
resultIntent.putExtras(mExtras);
resultIntent.setPackage(mContext.getPackageName());
mContext.sendBroadcast(resultIntent);
}
// The task is complete so remove it from the running tasks list
mTasks.remove(mTaskIdentifier);
// If there are no other executing methods then stop the service
if (mTasks.size() < 1)
{
stopSelf(lastStartId);
}
}
}
}
}
Now if you browse to the code that contain the AsyncService, and puts the resultIntent.put(Extras.RESULT_EXTRA, true);
Now how should i pass the data back to the intent. I heard Serializable is bad, and Parceable is ugly code. What else can i use. Secondly, where do i add the SQL cache retrieve code. How can i add this code to the framework. Hope i make sense.

android in app billing how to start the service and a few doubts

I seem to have trouble with the In app billing for android
I followed this guideAndroid Google Play In-App Billing which was really helpful in setting it up but i don't understand it exactly
what id like to know is
can i start the service by the user clicking a certain button and how do i start the service , also is there a way to set it if the user has not bought the content that i could maybe show a dialog box and destroy it when the user has bout the app
any advice or help will be much appreciated
here is my code:
package com.IrishSign.app;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import com.IrishSign.app.BillingService.RequestPurchase;
import com.IrishSign.app.BillingService.RestoreTransactions;
import com.IrishSign.app.Consts.PurchaseState;
import com.IrishSign.app.Consts.ResponseCode;
public class IrishSignAppActivity extends Activity {
/**
* The SharedPreferences key for recording whether we initialized the
* database. If false, then we perform a RestoreTransactions request
* to get all the purchases for this user.
*/
private static final String DB_INITIALIZED = "db_initialized";
private AppPurchaseObserver appPurchaseObserver;
private Handler handler;
private BillingService billingService;
private PurchaseDatabase purchaseDatabase;
private Set<String> ownedItems = new HashSet<String>();
/**
* The developer payload that is sent with subsequent
* purchase requests.
*/
private String payloadContents = null;
/**
* Each product in the catalog is either MANAGED or UNMANAGED. MANAGED
* means that the product can be purchased only once per user (such as a new
* level in a game). The purchase is remembered by Android Market and
* can be restored if this application is uninstalled and then
* re-installed. UNMANAGED is used for products that can be used up and
* purchased multiple times (such as poker chips). It is up to the
* application to keep track of UNMANAGED products for the user.
*/
private enum Managed { MANAGED, UNMANAGED }
/**
* A {#link PurchaseObserver} is used to get callbacks when Android Market sends
* messages to this application so that we can update the UI.
*/
private class AppPurchaseObserver extends PurchaseObserver {
public AppPurchaseObserver(Handler handler) {
super(IrishSignAppActivity.this, handler);
}
public void onBillingSupported(boolean supported) {
if (Consts.DEBUG) {
Log.i(TAG, "supported: " + supported);
}
if (supported) {
restoreDatabase();
//ADD enabling methods or anything else you may need here...
}
//YOU can also put a dialog here
}
#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 (purchaseState == PurchaseState.PURCHASED) {
ownedItems.add(itemId);
}
//YOU can also add other checks here
}
#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");
}
//Do something here if you want
} 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 = getSharedPreferences("idroprbilling",Context.MODE_PRIVATE);//NOTE: Change the idroprbilling to something else
SharedPreferences.Editor edit = prefs.edit();
edit.putBoolean(DB_INITIALIZED, true);
edit.commit();
} else {
if (Consts.DEBUG) {
Log.d(TAG, "RestoreTransactions error: " + responseCode);
}
}
}
#Override
public void onBillingSupported(boolean supported, String type) {
// TODO Auto-generated method stub
}
}
//NOTE: This may not even apply to your app - copied here but may never be used if only purchasing an UPGRADE or PREMIUM service
private static class CatalogEntry {
public String sku;
public int nameId;
public Managed managed;
public CatalogEntry(String sku, int nameId, Managed managed) {
this.sku = sku;
this.nameId = nameId;
this.managed = managed;
}
}
//NOTE: Same as above - An array of product list entries for the products that can be purchased.
private static final CatalogEntry[] CATALOG = new CatalogEntry[] {
new CatalogEntry("Upgrade", R.string.upgrade, Managed.MANAGED),
new CatalogEntry("android.test.purchased", R.string.android_test_purchased, Managed.UNMANAGED),
new CatalogEntry("android.test.canceled", R.string.android_test_canceled, Managed.UNMANAGED),
new CatalogEntry("android.test.refunded", R.string.android_test_refunded, Managed.UNMANAGED),
new CatalogEntry("android.test.item_unavailable", R.string.android_test_item_unavailable, Managed.UNMANAGED),
};
/**
* If the database has not been initialized, we send a
* RESTORE_TRANSACTIONS request to Android Market to get the list of purchased items
* for this user. This happens if the application has just been installed
* or the user wiped data. We do not want to do this on every startup, rather, we want to do
* only when the database needs to be initialized.
*/
private void restoreDatabase() {
SharedPreferences prefs = getSharedPreferences("idroprbilling",MODE_PRIVATE);
boolean initialized = prefs.getBoolean(DB_INITIALIZED, false);
if (!initialized) {
billingService.restoreTransactions();
Toast.makeText(this, R.string.restoring_transactions, Toast.LENGTH_LONG).show();
}
}
/**
* 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
*/
//NOTE: This method is here for convenience only
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;
}
#Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
//Now setup the in-app billing</pre>
handler = new Handler();
appPurchaseObserver = new AppPurchaseObserver(handler);
billingService = new BillingService();
billingService.setContext(this);
purchaseDatabase = new PurchaseDatabase(this);
ResponseHandler.register(appPurchaseObserver);
/**
* Creates a background thread that reads the database and initializes the
* set of owned items.
*/
//Check if billing is supported. (Optional)
boolean check = billingService.checkBillingSupported();
setContentView(R.layout.main);
Button Language = (Button) findViewById(R.id.language);
Button A = (Button) findViewById(R.id.alphabet);
Button purchaseableItem = (Button) findViewById(R.id.topics);
Button purchaseableItem2 = (Button) findViewById(R.id.intro);
Button G = (Button) findViewById(R.id.about);
G.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// TODO Auto-generated method stub
AlertDialog alertDialog = new AlertDialog.Builder(
IrishSignAppActivity.this).setCancelable(false)
.create(); // Reads Update
alertDialog.setTitle("Welcome ");
alertDialog.setMessage("this is my first dialog box :)");// Change tommorow//
alertDialog.setButton("Continue..",
new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int arg1) {
Intent our3intent = new Intent(
"com.IrishSign.app.MAIN"); // Change to
// revert
// back to
// menu
startActivity(our3intent);
}
});
alertDialog.show(); // <-- Shows dialog on screen.
}
});
purchaseableItem2.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// TODO Auto-generated method stub
Intent our3intent = new Intent("com.IrishSign.app.Intro");
startActivity(our3intent);
}
});
purchaseableItem.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// TODO Auto-generated method stub
//Now setup the in-app billing</pre>
boolean val = billingService.requestPurchase("com.irishsign.app.topicvideos", payloadContents, null);
}
});
A.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// TODO Auto-generated method stub
Intent ourintent = new Intent("com.IrishSign.app.alpha");
startActivity(ourintent);
}
});
Language.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
Intent language = new Intent("com.IrishSign.app.Language");
startActivity(language);
}
});
}
}
The In-app billing sample implementation from Google is overly complex and uses unorthodox methods in a couple of areas. I can't explain why they did it this way but this is how I've worked around it.
First you can assume the service is 'started' and ready to be used when you initialised it in onCreate and gave it a context.
You load the Google Market purchase screen by using this line:
this.billingService.requestPurchase(<String ID of the in-app product>, null);
If the user purchases the item successfully, Market will broadcast a message to AppPurchaseObserver.onPurchaseStateChange() that you have set up to listen for them. In onPurchaseStateChange(), you'll want to do two things:
Store the fact that this user now owns this item in your own datastore on the app.
Update current activity to show new purchase (i.e. the dialog you are showing to the user that they don't own the item can now be closed.). Because AppPurchaseObserver is a nested class within your activity, onPurchaseStateChange() has access to the activity's view objects.
The thing to realise is that there is no way to query what items the user has purchased from Market in a fast, constant manner so Google actually requires that you store this info in your own data store when notified (as per 1. above). When you load up your own activities, you just query your own datastore of what the user purchased and load the right visual cues accordingly.

Does the first MapActivity instance always leak?

While investigating memory issues in our application, it turns out that if the application Activity is a MapActivity, the first instance of it won't be finalized. Leading to other memory leak such as the view passed to setContentView.
Does anyone notice that before?
Here is the testing code showing that "MainActivity : 1" is not finalized whereas it is if MainActivity inherits from Activity.
To test, one needs to change device or emulator orientation many times.
import com.google.android.maps.MapActivity;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
public class MainActivity extends MapActivity {
private static final String defaultTag = "MA";
private static final boolean isDebugModeActivate = true;
private static final boolean isClassTagDisplayed = false;
private static final boolean isWebModeActivate = false;
static public void d(Object thiso, String message)
{
String tag = defaultTag + (isClassTagDisplayed == true ? "_" + thiso.getClass().getSimpleName() : "");
message = (isClassTagDisplayed == false ? thiso.getClass().getSimpleName() + " : " : "") + message;
Log.d(tag, message);
}
public MainActivity()
{
counter++;
uid++;
id = uid;
d(this, id + " tst constructor (" + counter + ")");
}
private static int counter = 0;
private static int uid = 0;
private final int id;
protected void finalize() throws Throwable
{
counter--;
d(this, id + " tst finalize (" +counter + ") ");
super.finalize();
}
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
#Override
protected boolean isRouteDisplayed()
{
return false;
}
}
Thank you,
David
Perhaps you should exchange notes with NickT here

Categories

Resources