Monday, July 18, 2016

Known issues as of Google Play Services 9.2

  1. NullPointerException: at com.google.android.gms.common.internal.zzi.onClick.
    For details see here for work around:
    http://stackoverflow.com/questions/38166820/google-play-services-9-2-0-causes-undesired-crash-in-device-without-google-play
     
  2.  NullPointerException: Attempt to invoke virtual method 'byte[] java.lang.String.getBytes()' on a null object reference android.util.Base64.decode.
    For details see here (no work around):
    http://stackoverflow.com/questions/37361651/firebase-crash-library-nullpointerexception-in-the-console

Thursday, December 17, 2015

Selecting native animation view

When creating an Android native (non-OpenGL) animation on a view, there are few options:
  • SurfaceView
  • TextureView (Android 4.0+)
  • View
While in Android 2.x SurfaceView was the best way, things changed. I'll skip to the bottom line: to my opinion as of Android 6.0 the best was is to override View. The reason is that both SurfaceView & TextureView do not support Android's hardware acceleration.

Hardware accelerated View is over x4 faster than SurfaceView & TextureView.

Sunday, June 7, 2015

AdMob interstial adapter for Appnext

I needed intergration Appnext into AdMob mediation bt there was not AdMob adapter, so I wrote one. Please note this adapter uses Appnext's Activities & not in-activity popups.


package com.appnext.appnextsdk;

import android.content.Context;
import android.os.Bundle;

import com.google.android.gms.ads.mediation.MediationAdRequest;
import com.google.android.gms.ads.mediation.customevent.CustomEventInterstitial;
import com.google.android.gms.ads.mediation.customevent.CustomEventInterstitialListener;

public class AppnextAdMobAdapter implements CustomEventInterstitial {
 private CustomEventInterstitialListener mCustomEventInterstitialListener;
 private String mPlacementId;
 private Context mContext;
 
 @Override
 public void onDestroy() {
 }

 @Override
 public void onPause() {
 }

 @Override
 public void onResume() {
 }

 @Override
 public void requestInterstitialAd(Context context,
            CustomEventInterstitialListener listener,
            String serverParameter,
            MediationAdRequest mediationAdRequest,
            Bundle customEventExtras) {
  mContext = context;
  mPlacementId = serverParameter;
  mCustomEventInterstitialListener = listener;

  if (mCustomEventInterstitialListener != null)
   mCustomEventInterstitialListener.onAdLoaded();
 }

 @Override
 public void showInterstitial() {
  PopupActivity.setAdLoadInterface(new OnAdLoadInterface() {   
   @Override
   public void adLoaded() {
    if (mCustomEventInterstitialListener != null)
     mCustomEventInterstitialListener.onAdLoaded();
   }
  });
  PopupActivity.setNoAdsInterface(new NoAdsInterface() {   
   @Override
   public void noAds() {
    if (mCustomEventInterstitialListener != null)
     mCustomEventInterstitialListener.onAdFailedToLoad(0);
   }
  });
  PopupActivity.setPopupOpenedInterface(new PopupOpenedInterface() {   
   @Override
   public void popupOpened() {
    if (mCustomEventInterstitialListener != null)
     mCustomEventInterstitialListener.onAdOpened();
   }
  });
  PopupActivity.setPopupClickedCallback(new PopupClickedInterface() {
   @Override
   public void popupClicked() {
    if (mCustomEventInterstitialListener != null)
     mCustomEventInterstitialListener.onAdClicked();
   }
  });
  PopupActivity.setPopupClosedCallback(new PopupClosedInterface() {   
   @Override
   public void popupClosed() {
    if (mCustomEventInterstitialListener != null)
     mCustomEventInterstitialListener.onAdClosed();
   }
  });

  Appnext.showPopupInActivity(mContext, mPlacementId, false);
 }
}

Tuesday, April 7, 2015

Detecting mobile app CPI frauds

Most CPI campaign work quite nicely. However, I saw this video which claim major click-frauds on Facebook ads from click-farms in specific countries. I've decided to re-check my last CPI campaign.

How I checked: by country, check the average session duration during the campaign. Filter only countries with average session duration less than 50% of the average.

Finding: At least 2 of the top 10 countries in the campaign clearly had a CPI fraud. The average session duration was less than 50% of the average, the CTR was x2-x4. Next time I'll look for it in real time. I've compared it with the data after the campaign & the session duration went back to normal.

Waste of money.

Bottom line: How I will search for ad frauds, at country level:
  1. Significantly low (or high) session duration
  2. Significantly high ad CTR rate

Sunday, January 11, 2015

Calculating Google Analytics event value variance & standard deviation

Here's I calculated the var & stddev of an event value (X) tracked by Google Analytics:
  1. Add a second event with the value X^2
  2. Calculate the variance using this formula: Var = E(X^2) - [E(X)]^2  (note that Google Analytics already calculates E(X^2) & E(X), which are, in this case the event's average).

Friday, November 28, 2014

Can Google Play Services introduce bugs to my apps without code update?

Here's a short list of FC bugs I think Google Play Services introduced remotely to my apps:

Wednesday, September 3, 2014

Removing app from Amazon App Store

Few years ago I had few apps published on Amazon App Store. Since the performance on that market wasn't impressive I didn't update them over time. Few weeks ago I decided to unpublish those apps.

Problem: No way to unpublish apps from the Amazon App Store.
Solution 1: Contact Amazon support and ask them to remove the app. They might want reasons and such.
Solution 2: Limit app distribution to the smallest market you can find on their list.

Monday, April 7, 2014

Upgrading Google Analytics for Android v2 to v4

I couldn't find a single place where all this information can be found, so here are the steps to upgrade Google Analytics for Android v2 to v4 (Google Play Services). This does not include the steps needed to integrate Google Play Services (some Manifest.xml & library includes):

  1. Move the analytics.xml from /res/values to /res/xml
  2. Add the following line to the Manifest.xml:
    <meta-data android:name="com.google.android.gms.analytics.globalConfigResource" android:resource="@xml/analytics" />
    
  3. Replace the campaign tracker in the Manifest.xml if need:
    <service android:name="com.google.android.gms.analytics.CampaignTrackingService" />
    <receiver android:name="com.google.android.gms.analytics.CampaignTrackingReceiver"
              android:exported="true">
       <intent-filter>
      <action android:name="com.android.vending.INSTALL_REFERRER" />
      </intent-filter>
    </receiver>
    
  4. Add the following code to the Application class:
  5. private Tracker mTracker = null;
    
    synchronized public Tracker getTracker() {
      if (mTracker == null) {
        GoogleAnalytics analytics = GoogleAnalytics.getInstance(this);
        mTracker = analytics.newTracker(R.xml.analytics);
      }
      
      return mTracker;
    }
    
  6. Activity tracking - replace:
    @Override
    public void onStart() {
      super.onStart();
      
      EasyTracker.getInstance().activityStart(this);
      mTracker = EasyTracker.getTracker();
    }
    
    @Override
    public void onStop() {
      super.onStop();
      
      EasyTracker.getInstance().activityStop(this);
    }
    
    
    with:
    @Override
    public void onStart() {
      super.onStart();
      
      mTracker = ((MyApplication)getApplication()).getTracker();
      GoogleAnalytics.getInstance(this).reportActivityStart(this);
    }
     
    @Override
    public void onStop() {
      super.onStop();
      
      GoogleAnalytics.getInstance(this).reportActivityStop(this);
    }
    
  7. Update events tracking to the new format:
  8. mTracker.send(new HitBuilders.EventBuilder()
      .setCategory(category)
      .setAction(action)
      .setLabel(label)
      .setValue(val)
      .build());
    

Sunday, December 29, 2013

java.lang.IllegalArgumentException: Can't pass bindargs for this sql :insert into....

I've optimized one of my Android apps to use compiled statement (SQLiteStatement) instead of direct SQL to improve performance.

After doing so, I started to get some unhandled exception reports, which had the following in common: Android 4.0.3, Turkish locale and the following stack trace:
java.lang.IllegalArgumentException: Can't pass bindargs for this sql :insert into android.database.sqlite.SQLiteProgram.compileAndbindAllArgs(SQLiteProgram.java)
android.database.sqlite.SQLiteStatement.acquireAndLock(SQLiteStatement.java)
android.database.sqlite.SQLiteStatement.executeUpdateDelete(SQLiteStatement.java)
android.database.sqlite.SQLiteStatement.execute(SQLiteStatement.java)
 
Solution: When using SQLiteStatement write SQL in uppercase letters.

Reason: After searching for some time I found this issue. The SQLiteStatement checks the type of the SQL inside. The checking process is simple string compare of the SQL prefix (INSERT/UPDATE/SELECT/etc..). The string compare in Android 4.0.3 seem to be culture dependent, and in Turkish locale uppercase 'insert' translate to 'İNSERT' (note the dot above the Turkish I') which gives an error in SQL.

Wednesday, December 4, 2013

Notes on using Google Play Game Services

Here are few tips & tricks that helped me [on going post]:

Google Drive: This spreadsheet contains too much content and can no longer be edited

I got this error ("This spreadsheet contains too much content and can no longer be edited") while using ACRA reporting to Google Drive (legacy). In general you should prevent your spreadsheet from reaching the maximum limits (see this for ACRA example). Once you get this error - you can't clean the spreadsheet anymore.

Solution: It's a bypass, not a real solution - click on "File" => "See revision history". Select a revision that should work and click "Restore this revision". You'll lose your latest information, but the spreadsheet will be writable. From now on make sure you're not getting anything close to the limits.

Wednesday, May 22, 2013

How to open calendar intent programmatically

I was looking around for a way to open the calendar application for viewing. The closest solution I found was running the stock calendar app explicitally using the package name.

Here's the best solution I came up with to open for viewing the Android calendar:
Intent i = new Intent(Intent.ACTION_VIEW);
// Android 2.2+
i.setData(Uri.parse("content://com.android.calendar/time"));  
// Before Android 2.2+
//i.setData(Uri.parse("content://calendar/time"));  

startActivity(i);

Wednesday, May 15, 2013

Error after updating Android's ADT 22 plug-in on Eclipse

After updating to the latest ADT version on Eclipse Juno, Windows 7, I got the following error:
 
Error Loading the SDK:

Error: Error parsing the sdk.
Failed to create C:\Program Files (x86)\Android\android-sdk\build-tools

Solution: To my best of understanding the ADT plugin is trying to change files in location the current user can't. I'm sure there are better solutions, but this is the quickest:
1. Go to folder 'C:\Program Files (x86)\Android\'
2. Right click on 'android-sdk' => 'Properties' => 'Security'
3. Click on 'Users' and then 'Edit', and add 'Full Control' and hit 'OK'

After Windows finish changing the permissions, restart Eclipse.

Saturday, March 9, 2013

BitmapFactory.decodeResource performance

While checking the performance of my app I've noticed BitmapFactory.decodeResource is relatively slow. So I've checked the alernative, instead of loading my PNG files as resources, putting them in /assets/ folder and loading using BitmapFactory.decodeStream.

Here are the test functions:

void decodeResource() {
 Bitmap originalBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.tile, null);
 originalBitmap.recycle();
}
void decodeStream() {
 InputStream ins = null;
 try {
  ins = getAssets().open("tile.png");

  Bitmap originalBitmap = BitmapFactory.decodeStream(ins);
  originalBitmap.recycle();
 } catch (final IOException e) {
  e.printStackTrace();
 } finally {
  if (ins != null)
   try {
    ins.close();
   } catch (IOException e) { }
 }     
}

Running both functions 50 times to load a small PNG file (230*230) on Nexus Galaxy running Android 4.2.2:
  • decodeResource: 1793ms 
  • decodeStream: 188ms 
Conclusion: decodeStream is almost x10 faster than decodeResource.

Tuesday, February 12, 2013

Chartboost SDK & Proguard

UPDATE: This post (both issues) was made obsolete by Chartboost SDK 3.1.5. Follow the integration instructions supplied by Chartboost.

When using Chartboost SDK 3.1.3, ProGuard fails to compile with few warnings. To bypass this issue add the following lines to proguard.cfg:
# Chartboost
-dontwarn com.mongodb.tools.ConnectionPoolStat
-dontwarn com.mongodb.util.management.jmx.JMXMBeanServer
-dontwarn org.bson.types.ObjectId

IMPORTANT: If you're using mediation you might need to add more ProGuard exceptions.

Also, as of Chartboost Android SDK 3.1.3 there's a bug which prevent clicking Chartboost ads on Android 3.0+ (it will hang during the "Loading..." stage after the user clicks an ad). In the logcat the following exception appears: android.os.NetworkOnMainThreadException

Until Chartboost will fix it, just add the following patch to the onCreate method:
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
StrictMode.setThreadPolicy(policy); 
Note: this requires Android Platform SDK 9+

Sunday, February 10, 2013

Switching users when using Team Foundation Service & Visual Studio

I encountered this problem using Visual Studio 2010, but it might also apply to Visual Studio 2012, Eclipse & Team Foundation Everywhere. It's relevant only to the Team Foundation Service, not Server (for the server, the secret is to clear the "Credentials Manager").

I wanted to switch users, but I could not. Long story short, when I tried to log out I was automatically logged in, when I tried to open my other TFS site I got error TF31003 with the "Use different user" link missing.

The solution:
  1. Close all programs.
  2. Open "Internet Explorer".
  3. Click on "Settings" (the wheel on the right side in my version of IE).
  4. Under "Browsing History", click on "Delete", select "Passwords" and delete.
  5. Next time, NEVER EVER click on "Sign me in next time" in any of Microsoft's services.

Sunday, January 6, 2013

Advanced ACRA 3: Auto delete old records

One issue with using Google Spreadsheet for ACRA reports is the size limitation of Google Spreadsheet. Specifically, the maximum size of Google Spreadsheet is 400K cells (which translates to 11K reports as of ACRA 4.2.3). More over, if the Google Spreadsheet become too large it simply won't open (there are multiple post of people saying they just can't open large ACRA Spreadsheets).

To avoid this limitation - automatically delete old ACRA reports:
1. Add the following Google Apps Script to your ACRA Google Spreadsheet ("Tools" -> "Script Editor" -> "Blank Project").
2. Schedule the script to run once a day using Time-Driven trigger (in the Script Editor, "Resources"-> "Current script's triggers").

I've extended the script to:
*. remove old versions entries so I could focus on the latest version only, just adjust the version number parameter.
*. Remove invalid entries (I think that was solved in later version of ACRA).



function deleteObsoleteRows() {  
  // Application specific
  //  // https://developers.google.com/apps-script/class_spreadsheetapp#openById
  var SPREAD_SHEET_KEY = "ENTER YOUR KEY HERE";
  //  // Delete any recoprd below this version 
  var APP_VERSION_CODE_CURRENT = 0;

  // ACRA 4.2.3 version specific - report cell locations
  var APP_VERSION_CODE = 2;

  // Maximum records per spreadsheet - if there are more - delete the oldest records
  // Google Spreadhseet over too many records is not useable
  var MAX_RECORDS = 10000;
    
  // For details see 
  // https://developers.google.com/apps-script/class_spreadsheetapp#openById
  var sheet = SpreadsheetApp.openById(SPREAD_SHEET_KEY);
  //var sheet = SpreadsheetApp.getActiveSheet();
  
  var rows = sheet.getDataRange();
  var numRows = rows.getNumRows();
  var values = rows.getValues();

  // Delete invalid records
  for (var i = numRows - 1; i >= 1; i--) {
    var row = values[i];
    
    // Logger.log(row);
    
    // Delete empty APP_VERSION_CODE
    if (row[APP_VERSION_CODE] == "") 
      sheet.deleteRow(i+1);
    // Delete old version records
    else if (row[APP_VERSION_CODE] < APP_VERSION_CODE_CURRENT) 
      sheet.deleteRow(i+1);
  }
  
  // Delete the oldest records if there are too many records
  for (var i = numRows - MAX_RECORDS; i > 0; i--) {
    sheet.deleteRow(2);
  }
};

Wednesday, December 26, 2012

Binding bitmap to ImageView with SimpleAdapter

SimpleAdapter does not bind bitmaps to ImageView. That's the bottom line. From viewing the source of SimpleAdapter it seems it only binds text and integers (???) to ImageView.

I didn't like the extension I've found which do that (it overrides the whole binding function instead of just enhancing it). so I wrote my own.

package com.utils;

import java.util.List;
import java.util.Map;

import android.content.Context;
import android.graphics.Bitmap;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.SimpleAdapter;

public class SimpleAdapterEx extends SimpleAdapter {
 private int mResource;
    private int[] mTo;
    private String[] mFrom;

    private List<? extends Map<String, ?>> mData;
 
 public SimpleAdapterEx(Context context, List<? extends Map<String, ?>> data,
            int resource, String[] from, int[] to) {
  super(context, data, resource, from, to);
  
  mResource = resource;
  mData = data;
  mTo = to;
  mFrom = from;
 }

 @Override
    public View getView(int position, View convertView, ViewGroup parent) {
  View v = super.getView(position, convertView, parent);

  v = createViewFromResourceEx(v, position, convertView, parent, mResource);

  return v;
    }
 
 @Override
    public View getDropDownView(int position, View convertView, ViewGroup parent) {
  View v = super.getDropDownView(position, convertView, parent);
  
  v = createViewFromResourceEx(v, position, convertView, parent, mResource);
  
  return v;
 }
 
    private View createViewFromResourceEx(View v, int position, View convertView,      
            ViewGroup parent, int resource) {
     
        bindView(position, v);

     return v;
    }
        
    private void bindView(int position, View view) {
        final Map dataSet = mData.get(position);
        if (dataSet == null) {
            return;
        }

        final ViewBinder binder = getViewBinder();
        final String[] from = mFrom;
        final int[] to = mTo;
        final int count = to.length;

        for (int i = 0; i < count; i++) {
            final View v = view.findViewById(to[i]);
            if (v != null) {
                final Object data = dataSet.get(from[i]);
                String text = data == null ? "" : data.toString();
                if (text == null) {
                    text = "";
                }

                boolean bound = false;
                if (binder != null) {
                    bound = binder.setViewValue(v, data, text);
                }

                if (!bound) {
                    if (v instanceof ImageView) {
                        if (data instanceof Bitmap) {
                            setViewImage((ImageView) v, (Bitmap)data);                            
                        }                        
                    }
                }
            }
        }     
    }      
    
    private void setViewImage(ImageView imageView, Bitmap bitmap) {
     imageView.setImageBitmap(bitmap);
    }
}

Friday, December 21, 2012

Android Market Optimization: Part II

Earlier this year I wrote about Android Market Optimization blog posts. Now there are few sites that can help you do that:
In general you should follow ASO tag in your favorite tech blog, for example on TechCrunch.

Thursday, November 22, 2012

Eclipse: Unable to find Action Set

Problem: After updating Eclipse my Android plug-in stopped working. Checking a bit more, all my Eclipse plug-ins stopped working (they even didn't appear in the Help->About windows).

In the errors log window (Window->Show View->Error Log) I saw the error "Unable to find Action Set" for all the plug-ins. For example, the Android plug-ins errors:
Unable to find Action Set: adt.actionSet.lint
Unable to find Action Set: adt.actionSet.avdManager
Unable to find Action Set: adt.actionSet.refactorings
Unable to find Action Set: adt.actionSet.wizards

Solution: I checked many options, bottom line, the problem was solved in my case when I've uninstalled (Help->About->Installation Details) a package called: Object Teams Patch for JDT/Core