Tuesday, March 27, 2012

ADT 17 Build issues

Google, Google, Google... What should I do with you??? Don't you ever think a bit outside the scope, or is this some sort of a grand master plan to muscle us all to work your way?

I can tell you right now that your main issue regarding the build process of the Android projects in Eclipse is due to the fact that there are two class paths for the same project, one is the Android libs, the other is the IDE classpath. I think that as programmers you should know this is BAD PRACTICE!!!

I came across two major issues, the NoClassDefFoundException, and the terribly annoying jarlist.cache file.
  1. In the previous versions of the ADT the dependent jars where automatically exported, which caused the dalvik issues in the build, with the "duplicate files" error. So their solution was to cancel the export completely and leave the export management to Eclipse, which is a step in the right direction... Personally, I would simply resolve the dependencies and not import a duplicate dependency.

    SO, if you experience the NoClassDefFoundException, it is because your .class files from the project dependencies were not imported to the output folder of the final Android application project.

    Maven, Eclipse, Ant could all have a different dependencies resolution, and different hierarchy structure, plus your own projects hierarchy, brings one to the conclusion that to understand the export  dependencies  feature, you must know what the hell you are doing, and understand projects hierarchy, your build tool, and how it resolves its project's dependencies.

    In general it is always best to enable the export on project D, but there are some cases exporting on project A would be preferred. For example:

    I.  Projects A, B, C, uses an XML library, where would you export the library? See Answer 1.
    II. Project A is a framework module project that projects B, C uses? See Answer 2.

    Nuts ha? but there is no other way to be 100% sure you would not export the same .class file twice!
    It may be that Eclipse resolves this issues, but I've started to trust no one! I have an intermittent Android lib project with most of the dependencies, which are also marked as export, and the final project depend on it.

    To enable export for dependencies you go to:  Project -> Right Click -> Properties -> Java Build Path -> Export tab -> check the project you want to export.

    And as for this solution, I think it is a bad idea... very bad idea, why? just because, I don't feel like writing another 100 rows about how you should work, this is my advice, you can take it or leave it :)
  2. The second most annoying issue with ADT 17, is the jarlist.cache file. Brilliantly the ADT team decided they need some file for god knows whatever reason, (it is really beside the point)  and they have decided to place that file hard coded at ${ ProjectFolder}/bin/jarlist.cache, and because some of us Eclipse users, are used to the fact that the default output folder is ${ ProjectFolder}/bin/, we experience this issue. Problem is that this build action does not effect only Android projects builds, but also pure Java projects, which eventually causes the mess. Android default output folder is at ${ProjectFolder}/bin/classes, you can verify it, go to your android project .classpath file and change the output to "/blah/blah" and see what happens once you clean build... like magic the adt nature returns it to "/bin/classes".

    The solution for this mess: point the output of all your projects in the workspace to "/bin/classes", this solves it!

If you encounter more issues with the ADT 17 let me know, I'll add reference to it.

In general, it is a step in the right direction, but this is annoying as hell, they change the freaking project management every version... can't they formulate something stable? I really hope the next version would be better, I mean they turn 18 soon... ;)


Answer 1: Project D
Answer 2: The dependencies whom are unique to Project A, would be exported. common used dependencies would not, they would be exported in Project D!!    << == (This is the conclusion!!)

Thursday, March 15, 2012

Notify User about an Upgrade version at Android Play-Store

Well, here is my impression of an upgrade feature for my Android wrapping framework (Cyborg):

It compares the versionCode of the Play-Store apk, and if the version string starts with the letter 'F', the PlayStoreModule would invoke an upgrade dialog, and would open the Play-Store, in the proper application.

I think that all the pieces are here, except for the application id, this one I got by calling on the getPlayStoreAppDetails_Async(activity, true), I've received 10 applications details printed to the log, and one of them was mine, I took the app id and used it hard coded in the top layer application.

I use the Android market api.

Underwent a bit of refactoring at: 24-03-2012
package com.nu.art.software.android.modules.market;


import java.io.IOException;
import java.util.List;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;

import com.gc.android.market.api.MarketSession;
import com.gc.android.market.api.MarketSession.Callback;
import com.gc.android.market.api.model.Market.App;
import com.gc.android.market.api.model.Market.AppsRequest;
import com.gc.android.market.api.model.Market.AppsResponse;
import com.gc.android.market.api.model.Market.ResponseContext;
import com.nu.art.software.android.core.AndroidModule;
import com.nu.art.software.android.dialogs.ForceActionDialog;
import com.nu.art.software.android.modules.configuration.ApplicationConfiguration;
import com.nu.art.software.android.utils.IntentFactory;
import com.nu.art.software.android.wrapper.R;


public final class PlayStoreModule
    extends AndroidModule {

  protected static final String GoogleAccountToken_Key = "A Google Account Token";

  public static final String ApplicationPlaySotreId_Key = "Application Play Store ID";

  private final Object TokenMonitor = new Object();

  private MarketSession session;

  private String sessionToken;

  private Account googleAccount;

  private AccountManager accountManager;

  private ApplicationConfiguration configuration;

  private String deviceId;

  private String playStoreAppId;

  private Thread applicationDetailsThread;

  @Override
  protected void init() {
    deviceId = getGtalkAndroidId(getApplication());
    configuration = getModuleOrThrowException(ApplicationConfiguration.class);
    playStoreAppId = configuration.getValue(false, ApplicationPlaySotreId_Key, null);
    if (playStoreAppId == null)
      throw new IllegalStateException("Must add your Play-Store application id to the configuration with key: "
          + ApplicationPlaySotreId_Key);
    sessionToken = configuration.getValue(true, GoogleAccountToken_Key, null);
    logDebug("Loaded Google AuthToken: " + sessionToken);
    accountManager = AccountManager.get(getApplication().getApplicationContext());
    Thread googlePlayStoreAPI_LoadingThread = new Thread(new Runnable() {

      @Override
      public void run() {
        synchronized (PlayStoreModule.this) {
          session = new MarketSession();
          session.getContext().setAndroidId(deviceId);
          logDebug("Market Session - Initialized ");
        }
      }
    }"Google Play-Store API Initializer");
    googlePlayStoreAPI_LoadingThread.start();
  }

  private static final Uri URI = Uri.parse("content://com.google.android.gsf.gservices");

  private static final String ID_KEY = "android_id";

  public static String getGtalkAndroidId(Context ctx) {
    String params[] {ID_KEY};
    Cursor c = ctx.getContentResolver().query(URI, null, null, params, null);
    if (!c.moveToFirst() || c.getColumnCount() 2)
      return null;
    try {
      return Long.toHexString(Long.parseLong(c.getString(1)));
    catch (NumberFormatException e) {
      return null;
    }
  }

  private boolean setGoogleAccountToken(final Activity activity) {
    logDebug("Getting new AuthToken");
    Account[] accounts = accountManager.getAccountsByType("com.google");
    if (accounts.length == 0) {
      sessionToken = null;
      return false;
    }
    googleAccount = accounts[0];

    AccountManagerCallback<Bundle> callBack = new AccountManagerCallback<Bundle>() {

      @Override
      public void run(AccountManagerFuture<Bundle> accountManagerFuture) {
        Bundle bundle;
        try {
          bundle = accountManagerFuture.getResult();
          sessionToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
          logDebug("Google New AuthToken: " + sessionToken);
          configuration.putValue(true, GoogleAccountToken_Key, sessionToken);
          synchronized (TokenMonitor) {
            TokenMonitor.notify();
          }
        catch (OperationCanceledException e) {
          logError(e);
        catch (AuthenticatorException e) {
          accountManager.invalidateAuthToken("com.google", sessionToken);
        catch (IOException e) {
          logError(e);
        }
      }

    };
    accountManager.getAuthToken(googleAccount, "android", null, activity, callBack, null);
    return true;
  }

  public final void checkIfForceUpdateIsInOrder(final Activity activity) {
    checkIfForceUpdateIsInOrder(activity, false);
  }

  public final void checkIfForceUpdateIsInOrder(final Activity activity, final boolean list) {
    if (applicationDetailsThread != null)
      return;
    applicationDetailsThread = new Thread(new Runnable() {

      @Override
      public void run() {
        process();
        applicationDetailsThread = null;

      }

      private void process() {
        boolean waitForToken = false;
        // if (sessionToken == null)
        waitForToken = setGoogleAccountToken(activity);

        if (waitForToken)
          synchronized (TokenMonitor) {
            try {
              logDebug("Waiting for google account token");
              TokenMonitor.wait(20000);
            catch (InterruptedException e) {
              logError("Erroe while waiting for Account token", e);
              return;
            }
          }
        // Launch race condition workaround
        if (sessionToken == null)
          return;
        logDebug("Get Application Details");
        synchronized (PlayStoreModule.this) {
          getApplicationDetails(activity, list);
        }
      }

    }"Play-Store Application Details fetcher workaround Thread");
    applicationDetailsThread.start();
  }

  private final void getApplicationDetails(final Activity activity, boolean list) {
    session.setAuthSubToken(sessionToken);

    com.gc.android.market.api.model.Market.AppsRequest.Builder builder = AppsRequest.newBuilder();
    builder.setStartIndex(0);
    if (list) {
      builder.setQuery(getApplication().getName());
      builder.setEntriesCount(10);
    else {
      builder.setAppId(playStoreAppId);
      builder.setEntriesCount(1);
    }
    // builder.setWithExtendedInfo(true);
    logDebug("AppsRequest.Builder: " + builder);

    AppsRequest appsRequest = builder.build();
    session.append(appsRequest, new Callback<AppsResponse>() {

      @Override
      public void onResult(ResponseContext context, AppsResponse response) {
        List<App> apps = response.getAppList();
        logDebug("ResponseContext: " + context);
        logDebug("AppsResponse: " + response);
        App app;
        if (apps.size() != 1)
          return;

        app = apps.get(0);
        int latestVersionCodeFound = app.getVersionCode();
        String latestVersionFound = app.getVersion();
        if (latestVersionCodeFound <= getApplication().getVersionCode()) {
          logDebug("No upgrade in market.");
          return;
        }
        if (!latestVersionFound.startsWith("F")) {
          logDebug("Newer (NOT MANDATORY) version is now available in Google play store (v:" + latestVersionFound + ", vc:" + latestVersionCodeFound + ").");
          return;
        }
        logDebug("Newer (MANDATORY) version is now available in Google play store (v:" + latestVersionFound + ", vc:" + latestVersionCodeFound + ").");
        final ForceActionDialog dialog = getUpgradeDialog(activity);
        String body = dialog.getBody();
        body = body.replace("${version}", latestVersionFound);
        dialog.setBody(body);
        dialog.setOnClickListener(new OnClickListener() {

          @Override
          public void onClick(View arg0) {
            Thread thread = new Thread(new Runnable() {

              @Override
              public void run() {
                dialog.dismiss();
                Intent intent = IntentFactory.openMarketApplicationDetails(activity.getApplicationContext().getPackageName());
                activity.startActivity(intent);
              }
            }"Upgrade APK Installer");
            thread.start();
          }
        });
        dialog.showDialog();
      }
    });
    try {
      session.flush();
    catch (Exception e) {
      logError(e);
      sessionToken = null;
      toast("Error checking for Upgrade", LongToast);
    }
  }

  protected ForceActionDialog getUpgradeDialog(final Activity activity) {
    final ForceActionDialog dialog = new ForceActionDialog(activity, R.string.UpgradeRequired, R.string.NeedToUpgradeApplicationMessage,
        R.string.Upgrade);
    dialog.setCancelableFlag(false);
    return dialog;
  }
}
Java2html