Skip to content

Instantly share code, notes, and snippets.

@freakhill
Last active December 26, 2015 02:39
Show Gist options
  • Save freakhill/7079774 to your computer and use it in GitHub Desktop.
Save freakhill/7079774 to your computer and use it in GitHub Desktop.
Wakuwaku SDK client
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alignmentMode="alignBounds"
android:background="#0099cc"
android:orientation="vertical"
tools:context=".CouponActivity" >
<ImageView
android:id="@+id/coupon_image"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"
android:src="@drawable/ic_launcher" />
<Button
android:id="@+id/coupon_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="email" />
<Button
android:id="@+id/coupon_wallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="wallet" />
<Button
android:id="@+id/coupon_refuse"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="refuse" />
</LinearLayout>
package jp.ne.wakuwaku.test;
import jp.ne.wakuwaku.sdk.Wakuwaku;
import jp.ne.wakuwaku.sdk.Wakuwaku.Coupon;
import jp.ne.wakuwaku.sdk.Wakuwaku.InitException;
import jp.ne.wakuwaku.sdk.Wakuwaku.NoEmailProvidedException;
import jp.ne.wakuwaku.sdk.Wakuwaku.NoWalletInstalledException;
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.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup.LayoutParams;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Toast;
/**
* AndroidAnnotations will generate a class *CouponActivity_* that is the actual
* class to declare in the AndroidManifest.
*
* @author Johan Gall <johan.gall@gmail.com>
*
*/
public class CouponActivity extends Activity {
private final static String TAG = "coupon-activity";
private Coupon current_coupon;
private Handler handler_ = new Handler();
ImageView coupon_image;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_coupon);
init();
}
public void toast(final String text) {
handler_.post(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplicationContext(), text, Toast.LENGTH_LONG)
.show();
}
});
}
protected void coupon_email() {
new AsyncTask<Object, Object, Object>() {
@Override
protected Object doInBackground(Object... params) {
try {
Log.i(TAG, "sending coupon to email!");
if (!load_email())
return null;
if (current_coupon != null) {
try {
Log.i(TAG, "calling sdk to send the coupon");
current_coupon.toEmail();
toast("Coupon sent to email!");
finish();
} catch (NoEmailProvidedException e) {
Log.e(TAG, "no email provided!", e);
toast("Failed to send coupon!");
}
} else {
Log.w(TAG, "current coupon is null!");
}
} catch (RuntimeException e) {
Log.e(TAG, "runtime exception", e);
}
return null;
}
}.execute();
}
private boolean load_email() {
SharedPreferences sp = getPreferences(MODE_PRIVATE);
String email = sp.getString("email", "");
if (email.length() == 0) {
askEmailDialog();
return false;
}
Wakuwaku.setEmail(email);
return true;
}
private void askEmailDialog() {
final Activity act = this;
handler_.post(new Runnable() {
@Override
public void run() {
final AlertDialog.Builder alert = new AlertDialog.Builder(act);
alert.setTitle("email");
alert.setMessage("please provide a valid email address to receive coupons");
final EditText input = new EditText(act);
alert.setView(input);
alert.setPositiveButton("Ok",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
Log.i(TAG, "user provided email: "
+ input.getText().toString());
SharedPreferences sp = getPreferences(MODE_PRIVATE);
sp.edit()
.putString("email",
input.getText().toString())
.commit();
coupon_email();
}
});
alert.setNegativeButton("Cancel",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
Log.i(TAG, "user did not provide an email");
toast("canceled");
}
});
alert.show();
}
});
}
public static void launch_from_game(Context ctx) {
Intent intent = new Intent(ctx, CouponActivity.class);
intent.putExtra("launch_from_game", true);
ctx.startActivity(intent);
}
protected void coupon_wallet() {
new AsyncTask<Object, Object, Object>() {
@Override
protected Object doInBackground(Object... params) {
try {
try {
current_coupon.toWallet();
toast("Coupon sent to wallet!");
finish();
} catch (NoWalletInstalledException e) {
Log.e(TAG, "no wallet provided!", e);
toast("Failed to send coupon!");
}
} catch (RuntimeException e) {
Log.e(TAG, "runtime exception", e);
}
return null;
}
}.execute();
}
protected void init() {
Intent launched_from = getIntent();
final boolean launched_from_game = launched_from.getBooleanExtra(
"launch_from_game", false);
final Activity coupon_activity = this;
new AsyncTask<Object, Object, Object>() {
@Override
protected Object doInBackground(Object... params) {
try {
if (launched_from_game) {
Log.i(TAG, "launched from game.");
current_coupon = Wakuwaku.take();
displayCoupon();
} else {
Log.i(TAG, "not launched from game.");
try {
Wakuwaku.init(coupon_activity, "testg1", 10);
} catch (InitException e) {
Log.e(TAG,
"failed to initialize the WAKU-WAKU SDK", e);
throw new Error("Waku-Waku initializatoin failed after independant launch (not from game)!");
}
Wakuwaku.whenNoCouponLeft(new Runnable() {
@Override
public void run() {
Wakuwaku.fetch(1);
}
});
Wakuwaku.fetch(1);
handler_.postDelayed(new Runnable() {
@Override
public void run() {
current_coupon = Wakuwaku.take();
displayCoupon();
}
}, 1500L);
}
} catch (RuntimeException e) {
Log.e("CouponActivity_",
"A runtime exception was thrown while executing code in a runnable",
e);
}
return null;
}
}.execute();
}
protected void displayCoupon() {
new AsyncTask<Object, Object, Object>() {
@Override
protected Object doInBackground(Object... params) {
try {
// current_coupon.getDrawable() might involve network
// operations
// so we do it in background.
Log.i(TAG, "displaying coupon");
if(current_coupon == null) {
toast("no coupon fetched!");
Log.w(TAG, "no current coupon fetched!");
} else {
displayCoupon(current_coupon.getDrawable());
}
} catch (RuntimeException e) {
Log.e(TAG, "runtime exception", e);
}
return null;
}
}.execute();
}
protected void displayCoupon(final Drawable d) {
handler_.post(new Runnable() {
@Override
public void run() {
try {
Log.i(TAG, "displaying drawable from coupon");
coupon_image.setImageDrawable(d);
} catch (RuntimeException e) {
Log.e(TAG, "runtime exception", e);
}
}
});
}
private void afterSetContentView() {
coupon_image = ((ImageView) findViewById(R.id.coupon_image));
{
View view = findViewById(R.id.coupon_wallet);
if (view != null) {
view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
coupon_wallet();
}
});
}
}
{
View view = findViewById(R.id.coupon_email);
if (view != null) {
view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
coupon_email();
}
});
}
}
}
@Override
public void setContentView(int layoutResID) {
super.setContentView(layoutResID);
afterSetContentView();
}
@Override
public void setContentView(View view, LayoutParams params) {
super.setContentView(view, params);
afterSetContentView();
}
@Override
public void setContentView(View view) {
super.setContentView(view);
afterSetContentView();
}
}
/**
Copyright (c) 2013 WAKU WAKU. All rights reserved.
Johan Gall <johan.gall@waku-waku.ne.jp>
*/
/**
* WAKU WAKU client SDK
* ===
* Gist available at https://gist.github.com/freakhill/7079774
* contains:
* Wakuwaku.java - main java file
* example files (activity + layout)
* wakuwaku-sdk.jar - (already contains Wakuwaku.java)
*/
package jp.ne.wakuwaku.sdk;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.lang.ref.SoftReference;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashSet;
import java.util.NoSuchElementException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import org.json.JSONException;
import org.json.JSONObject;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.support.v4.util.LruCache;
import android.telephony.TelephonyManager;
import android.util.Base64;
import android.util.Log;
/**
* WAKU WAKU client SDK - from API Level 9 (Android 2.3 GINGERBREAD) + Android
* Support Library r19
*
* Everything starts by initializing the WAKU WAKU client (
* {@link #init(Context, String)}. You can then fetch coupon (
* {@link #fetch(int)} or {@link #fetch(int, int)}) that will be reserved for
* your app for a dedicated lifetime (typically 10 minutes) after which they get
* released.
*
* To take a coupon, so that you can present it to the user you can use
* {@link #take()}. You can check out the lifetime left for this coupon through
* {@link Coupon#timeLeft_ms()}. You can voluntarily prematurely release a
* coupon through {@link #release(Coupon)}. By default 15 seconds are retracted
* from the coupon's lifetime before automatic release so that your user would
* not lose a coupon by taking it a few instants before its release..
*
* For the user to obtain the real coupon content (and not a generic campaign
* image) a coupon need to be bound to either an email or a wallet (through
* {@link Coupon#toEmail()} or {@link Coupon#toWallet()}). After which the user
* will either receive said coupon in his mailbox or in his WAKU WAKU Wallet.
*
* When no coupon is left, by default the client does nothing. This behaviour is
* configurable by setting a callback through
* {@link #whenNoCouponLeft(Runnable)}. You can use this callback to fetch
* coupons. Be careful of battery usage (you can use
* {@link Helpers#radioState()} and you should consider your application life
* cycle to minimize radio usage).
*
* You can test whether the wallet is installed through
* {@link #walletInstalled()}.
*
* PS: there are a few helpers documented in {@link Helpers}
*
* You can set the email used by the client through {@link #setEmail(String)}.
* It will be stored in Shared Preferencies.
*
* ====================
*
* You need to provide an UI to this API.
*
* Display the generic image, a button to send to the wallet (and install it if
* necessary), a button to send by email (and set it if necessary).
*
*
* TLDR:
*
* 1. {@link #init(Context, String, int)}
*
* 2. optional - {@link #setEmail(String)}, {@link #installWallet()}
*
* 3. {@link #whenNoCouponLeft(Runnable)}
*
* 4. {@link #fetch(int)}
*
* 5. {@link #take()}
*
* 6. {@link Coupon#toWallet()} or {@link Coupon#toEmail()}
*
* -- check exceptions and if necessary
*
* {@link #installWallet()} or {@link #installWalletWithCoupon(Coupon)} if
* {@link #walletInstalled()} false and wallet required (sending coupon to
* wallet)
*
* {@link #setEmail(String)} if {@link #hasEmail()} false and email required
* (sending coupon to email)
*
* ====================
*
* For readability in case you want to go through this source code, private
* method are_in_this_case, and public method inThisOne.
*
* @author Johan Gall <johan.gall@waku-waku.ne.jp>
* @version 0.9
*
*/
@SuppressLint("NewApi")
public final class Wakuwaku {
public static String TAG = "wakuwaku-sdk";
// internals (impl details)
private static final Object mutex = new Object();
private static final Object release_mutex = new Object();
private static final Lock fetching_lock = new ReentrantLock();
private static final Condition fetch_done = fetching_lock.newCondition();
private final static String SHARED_PREFERENCES = "WakuwakuSdkSharedPrefsFile";
private static final int READ_TIMEOUT = 5000;
private static final int CONNECT_TIMEOUT = 5000;
private static LruCache<URL, Drawable> bitmap_cache;
private static Context ctx;
private static Runnable noCouponLeftCallback;
private static Timer timer;
private static boolean init_done = false;
// business logic?
private final static String coreUrl = "http://core.waku-waku.ne.jp:2929";
private final static String WALLET_PACKAGE_NAME = "jp.ne.wakuwaku.wallet";
private final static String BIND_COUPON = "jp.ne.wakuwaku.intent.BIND_COUPON";
private final static int COUPON_EXPIRATION_SECURITY_MARGIN = 15;
private final static int COUPON_TO_WALLET_WAIT_LIFETIME = 7 * 24 * 3600; // 1week
private final static ConcurrentSkipListSet<Coupon> coupons = new ConcurrentSkipListSet<Wakuwaku.Coupon>();
private static PublicKey coreKey;
private static String gid; // game id
private static String email;
// Exceptions
public final static class NoEmailProvidedException extends Exception {
private static final long serialVersionUID = -7185714616460385773L;
}
public final static class NoWalletInstalledException extends Exception {
private static final long serialVersionUID = 1724680393414253020L;
}
public static class CryptoException extends Exception {
private static final long serialVersionUID = -9002198609504059332L;
public CryptoException(Throwable e) {
super(e);
}
}
public final static class InitException extends Exception {
private static final long serialVersionUID = 5627168074447388555L;
}
public final static class InitRuntimeException extends RuntimeException {
private static final long serialVersionUID = -5114352830441926515L;
}
/**
* Release a coupon
*
* @param c
* coupon to release
*/
public static void release(Coupon c) {
synchronized (release_mutex) {
Log.i(TAG, "removing coupon: " + c.uuid);
coupons.remove(c);
if (coupons.isEmpty()) {
Log.i(TAG, "calling 'no coupon left callback'");
noCouponLeftCallback.run();
}
}
}
private static void add_coupon(Coupon c) {
Log.i(TAG, "adding coupon in local store: " + c.uuid);
if (!coupons.add(c))
Log.w(TAG, "coupon was already present in coupon set. uuid: "
+ c.uuid);
}
/**
*
* @return A coupon, or null if no coupon is available.
*/
public static Coupon take() {
Log.i(TAG, "taking a coupon");
try {
return coupons.first();
} catch (NoSuchElementException e) {
Log.w(TAG, "no coupon available to take!");
return null;
}
}
/**
* Initializes a SDK. Notable: initializes a LRU cache for bitmaps, and an
* extra timer (thus thread).
*
* @param ctx
* Context used for operations (getting connectivity manager
* etc.).
* @param gameId
* Your game id (registered in the WAKU WAKU server).
* @param bitmap_cache_size
* How many coupon images at max can be loaded.
* @throws InitException
*/
public static void init(Context ctx, String gameId, int bitmap_cache_size)
throws InitException {
synchronized (mutex) {
Log.i(TAG, "initializing Wakuwaku SDK client");
Wakuwaku.ctx = ctx;
gid = gameId;
load_core_key();
load_shared_preferences();
whenNoCouponLeftDoNothing();
Log.i(TAG, "creating generic campaign image cache");
bitmap_cache = new LruCache<URL, Drawable>(bitmap_cache_size);
Log.i(TAG, "creating Timer");
timer = new Timer();
init_done = true;
Log.i(TAG, "init done");
}
}
/**
* Sets a callback to execute when all coupons are released.
*
* @param r
* Callback to execute.
*/
public static void whenNoCouponLeft(Runnable r) {
noCouponLeftCallback = r;
}
/**
* Feeds a callback that does nothing to {@link #whenNoCouponLeft(Runnable)}
* .
*/
public static void whenNoCouponLeftDoNothing() {
whenNoCouponLeft(new Runnable() {
@Override
public void run() {
}
});
}
/**
* @param coupons
* maximum number of coupons to load
*/
public static void fetch(int coupons) {
fetch(coupons, Integer.MAX_VALUE);
}
/**
* Asynchronous! Fetches *up to* "coupons" coupons and *up to* "images"
* images!
*
* @param coupons
* maximum number of coupons to load
* @param images
* maximum number of images at one point in memory
*/
public static void fetch(final int coupons, final int images) {
synchronized (mutex) {
Log.i(TAG, "acting on request for " + coupons
+ " coupons, with less than " + images
+ " images in memory.");
ensure_init();
if (images > bitmap_cache.maxSize()) {
Log.w(TAG,
"cache maximum size is smaller than the number of images that might get loaded");
}
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
fetching_lock.lock();
try {
int current_image_count = countImages();
for (int i = 0; i < coupons; i++) {
Coupon c = fetch_coupon();
if (c == null)
continue;
if (current_image_count < images) {
if (c.loadDrawable()) {
current_image_count++;
}
}
fetch_done.signal();
}
return null;
} finally {
fetching_lock.unlock();
}
}
}.execute();
}
}
/**
* @author Johan Gall <johan.gall@waku-waku.ne.jp>
* @version 0.9
*
*/
public static final class Coupon implements Comparable<Coupon> {
private URL url;
private String code;
private String uuid;
private SoftReference<Drawable> d;
private long release_ms;
private long lifetime;
private Coupon(URL url, String code, String uuid, long lifetime) {
Log.i(TAG, "creating coupon with uuid: " + uuid + " and lifetime: "
+ lifetime);
lifetime -= COUPON_EXPIRATION_SECURITY_MARGIN;
Log.i(TAG, "retracted " + COUPON_EXPIRATION_SECURITY_MARGIN
+ "s from lifetime for coupon: " + uuid);
this.url = url;
this.code = code;
this.uuid = uuid;
release_ms = System.currentTimeMillis() + (lifetime * 1000);
schedulelifetimeExtend(release_ms);
this.lifetime = lifetime;
Wakuwaku.add_coupon(this);
}
/**
* @return Time left to the coupon before automatic release (in
* milliseconds). The 15s security margin is already accounted
* for (retracted from the original value fed by the server).
*/
public long timeLeft_ms() {
return Math.min(release_ms - System.currentTimeMillis(), 0);
}
/**
* Binds a coupon to an email (and sends it to that email).
*
* @return whether the operation succeeded or failed
* @throws NoEmailProvidedException
*/
public boolean toEmail() throws NoEmailProvidedException {
return Wakuwaku.to_email(this);
}
/**
* Binds a coupon to a wallet (sends it to that wallet).
*
* @return whether the operation succeeded or failed
* @throws NoWalletInstalledException
*/
public boolean toWallet() throws NoWalletInstalledException {
return Wakuwaku.to_wallet(this);
}
/**
* Returns a drawable containing the generic campaign content (image)
* associed to this coupon. Might force a download.
*
* WARNING!: Network activity may occur! Use on a background thread
*
* @return
*/
public Drawable getDrawable() {
loadDrawable();
return d.get();
}
/**
* @return true the drawable had to be downloaded
*/
private boolean loadDrawable() {
if (hasDrawableLoaded()) {
return false;
} else {
d = new SoftReference<Drawable>(download_image(url));
if (url == null || d.get() == null) {
Log.w(TAG, "failed to load drawable:url:" + no_null(url)
+ ":d.get:" + no_null(d.get()));
d = null;
return true;
}
bitmap_cache.put(url, d.get());
return true;
}
}
/**
* Indicates whether this coupon's image is already loaded.
*
* @return
*/
public boolean hasDrawableLoaded() {
if (d != null)
return true;
// internally we refer to the cache is something in there
// it is a side-effect of *hasDrawableLoaded* to make
// member *d* points to the cache if relevant
Drawable d_ = bitmap_cache.get(url);
if (d_ == null)
return false;
d = new SoftReference<Drawable>(d_);
return true;
}
private void schedulelifetimeExtend(long period) {
Log.i(TAG, "scheduling release for coupon: " + uuid);
final Coupon c = this;
TimerTask extend_task = new TimerTask() {
@Override
public void run() {
Wakuwaku.fetch_coupon(c.uuid, (int) c.lifetime);
}
};
timer.scheduleAtFixedRate(extend_task, period, period);
}
@Override
public boolean equals(Object o) {
if (o == null)
return false;
if (o instanceof Coupon) {
Coupon co = (Coupon) o;
return compareTo(co) == 0;
}
return false;
}
@Override
public int hashCode() {
return uuid.hashCode();
}
@Override
public int compareTo(Coupon another) {
if (another == null)
return 1;
if (uuid == null || another.uuid == null) {
if (uuid == another.uuid)
return 0;
return (uuid != null) ? 1 : -1;
}
return uuid.compareTo(another.uuid);
}
}
/**
* Sends an intent to install the wallet if not already installed (open the
* market etc.).
*/
public static void installWallet() {
if (walletInstalled()) {
return;
}
Log.i(TAG, "sending intent to install wallet");
Intent install_wallet = new Intent(Intent.ACTION_VIEW).setData(Uri
.parse("market://details?id=" + WALLET_PACKAGE_NAME));
ctx.startActivity(install_wallet);
}
/**
* Sends an intent to install the wallet if not already installed (open the
* market etc.) and sends it a coupon (parameter).
*
* @param c
* Coupon to give to the wallet.
*/
public static void installWalletWithCoupon(Coupon c) {
installWallet();
broadcast_sticky_coupon(c);
}
private static void broadcast_sticky_coupon(Coupon c) {
Log.i(TAG, "broadcasting sticky coupon with uuid: " + c.uuid);
ctx.sendStickyBroadcast(make_intent(c.uuid, c.code));
}
private static Intent make_intent(String uuid, String code) {
return new Intent(BIND_COUPON).putExtra("gid", gid)
.putExtra("uuid", uuid).putExtra("code", code);
}
/**
* @return whether the wallet is installed or not.
*/
public static boolean walletInstalled() {
PackageManager pm = ctx.getPackageManager();
try {
pm.getPackageInfo(WALLET_PACKAGE_NAME,
PackageManager.GET_ACTIVITIES);
return true;
} catch (NameNotFoundException e) {
return false;
}
}
private static Coupon fetch_coupon() {
return fetch_coupon(UUID.randomUUID().toString(), 60 * 5);
}
private static Coupon fetch_coupon(String uuid, int lifetime) {
Log.i(TAG, "fetching coupon");
// ---
URL url;
if ((url = make_url(coreUrl + "/1.0/coupon/taken/" + uuid)) == null)
return null;
// --- request body
JSONObject payload_json = new JSONObject();
try {
payload_json.put("uuid", uuid);
payload_json.put("gid", gid);
payload_json.put("lifetime", lifetime);
} catch (JSONException e) {
Log.e(TAG, "failed to compose json payload for bind request", e);
return null;
}
// --- encoded encrypted request body
String payload;
try {
payload = base64(public_core_encrypt(payload_json.toString()));
} catch (CryptoException e) {
Log.e(TAG, "failed to generate message for coupon fetch", e);
return null;
}
String respbody = post(url, payload);
if (respbody == null) {
Log.w(TAG, "failed to fetch coupon with url: " + url);
return null;
}
JSONObject response_json;
try {
response_json = new JSONObject(respbody);
} catch (JSONException e) {
Log.e(TAG, "failed to parse: " + respbody, e);
return null;
}
String curl;
try {
curl = response_json.getString("url");
} catch (JSONException e) {
Log.e(TAG, "failed to parse url in : " + respbody, e);
return null;
}
String code;
try {
code = response_json.getString("code");
} catch (JSONException e) {
Log.e(TAG, "failed to parse code in : " + respbody, e);
return null;
}
long response_lifetime;
try {
response_lifetime = response_json.getLong("lifetime");
} catch (JSONException e) {
Log.e(TAG, "failed to parse lifetime in : " + respbody, e);
return null;
}
URL gcurl;
if ((gcurl = make_url(curl)) == null) {
Log.w(TAG, "failed to parse url fetched from server: " + curl);
return null;
}
return new Coupon(gcurl, code, uuid, response_lifetime);
}
private static Drawable download_image(URL url) {
Log.i(TAG, "downloading image at: " + url);
InputStream in;
try {
in = url.openStream();
in = new BufferedInputStream(in);
} catch (IOException e) {
Log.e(TAG, "failed to open an input stream to: " + url, e);
return null;
}
Bitmap bmp = BitmapFactory.decodeStream(in);
try {
in.close();
} catch (IOException e) {
Log.e(TAG, "failed to close input stream to: " + url, e);
}
Log.i(TAG, "image downloaded from:" + url);
return new BitmapDrawable(ctx.getResources(), bmp);
}
private static URL make_url(String url) {
try {
return new URL(url);
} catch (MalformedURLException e) {
Log.e(TAG, "malformed url: " + url);
return null;
}
}
private static void load_core_key() throws InitException {
Log.i(TAG, "loading core key");
URL url = make_url(coreUrl + "/1.0/public-key");
if (url == null)
throw new InitException();
String key = get(url);
if (key == null) {
Log.w(TAG, "failed to load core key");
throw new InitException();
}
Log.i(TAG, "core key: " + key);
KeyFactory kf;
try {
kf = KeyFactory.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "key factory doesn't support RSA", e);
throw new InitException();
}
X509EncodedKeySpec ks = new X509EncodedKeySpec(Base64.decode(
key.replace("-----BEGIN PUBLIC KEY-----", "").replace(
"-----END PUBLIC KEY-----", ""), Base64.DEFAULT));
try {
coreKey = kf.generatePublic(ks);
} catch (InvalidKeySpecException e) {
Log.e(TAG,
"invalid key specification (base64nowrap x509 rsa public key)",
e);
throw new InitException();
}
}
private static void load_shared_preferences() {
Log.i(TAG, "loading shared preferences");
SharedPreferences sp = ctx.getSharedPreferences(SHARED_PREFERENCES,
Context.MODE_PRIVATE);
email = sp.getString("email", "");
}
private static HttpURLConnection make_conn(URL url) {
HttpURLConnection conn;
try {
conn = (HttpURLConnection) url.openConnection();
} catch (IOException e) {
Log.e(TAG, "failed to open connection to: " + url, e);
return null;
}
conn.setRequestProperty("Accept-Charset", "utf-8");
conn.setUseCaches(false);
conn.setReadTimeout(READ_TIMEOUT);
conn.setConnectTimeout(CONNECT_TIMEOUT);
conn.setDoInput(true);
return conn;
}
private static boolean is_network_up() {
ConnectivityManager connMgr = (ConnectivityManager) ctx
.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
return networkInfo != null && networkInfo.isConnected();
}
/**
* @param url
* @param payload
* @return response body is request successful (status 200), else null
*/
private static String post(URL url, String payload) {
Log.i(TAG,
"POST: " + (url != null ? url : "null") + " payload length: "
+ ((payload == null) ? -1 : payload.length()));
if (!is_network_up()) {
Log.i(TAG, "network is down, cannot execute post to: " + url);
return null;
}
HttpURLConnection conn = make_conn(url);
if (conn == null)
return null;
conn.setDoOutput(true);
// ---
try {
conn.setRequestMethod("POST");
} catch (ProtocolException e) {
Log.e(TAG, "POST is not supported", e);
return null;
}
// ---
OutputStream os;
BufferedWriter writer;
try {
os = conn.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "failed to open output stream on connection to: " + url);
return null;
}
try {
writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "unsupported UTF-8 encoding", e);
return null;
}
try {
writer.write(payload);
} catch (IOException e) {
Log.e(TAG, "failed to write payload (to connection): " + payload, e);
return null;
}
try {
writer.flush();
} catch (IOException e) {
Log.e(TAG, "failed to flush writer to connection: " + url, e);
return null;
}
try {
writer.close();
} catch (IOException e) {
Log.e(TAG, "failed to close writer to connection: " + url, e);
return null;
}
try {
return process_response(conn);
} finally {
conn.disconnect();
}
}
private static String process_response(HttpURLConnection conn) {
int respcode;
try {
respcode = conn.getResponseCode();
} catch (IOException e) {
Log.e(TAG, "failed to get response code from: " + conn.getURL(), e);
return null;
}
switch (respcode) {
case HttpURLConnection.HTTP_OK:
BufferedReader in;
try {
in = new BufferedReader(new InputStreamReader(
conn.getInputStream(), "UTF-8"));
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "unsupported UTF-8 encoding", e);
return null;
} catch (IOException e) {
Log.e(TAG, "failed to get input stream from connection to: "
+ conn.getURL(), e);
return null;
}
StringBuilder sb = new StringBuilder();
String line;
try {
while ((line = in.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
Log.e(TAG, "failed to read input stream from: " + conn.getURL()
+ " - read up to: " + sb.toString());
return null;
}
return sb.toString();
default:
Log.w(TAG, "request(" + conn.getURL()
+ ") unsuccesful, response code: " + respcode);
return null;
}
}
/**
* @param url
* @return response body is request successful (status 200), else null
*/
private static String get(URL url) {
Log.i(TAG, "GET: " + (url != null ? url : "null"));
if (!is_network_up()) {
Log.i(TAG, "network is down, cannot execute get to: " + url);
return null;
}
HttpURLConnection conn = make_conn(url);
if (conn == null)
return null;
// ---
try {
conn.setRequestMethod("GET");
} catch (ProtocolException e) {
Log.e(TAG, "GET is not supported", e);
return null;
}
// ---
return process_response(conn);
}
private static String base64(byte[] bytes) {
return Base64.encodeToString(bytes, Base64.DEFAULT);
}
private static byte[] utf8bytes(String in) {
try {
return in.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "UTF-8 encoding not supported.", e);
throw new RuntimeException(e);
}
}
private static byte[] public_core_encrypt(String in) throws CryptoException {
Cipher cipher;
String _cipher = "RSA/ECB/PKCS1Padding";
try {
cipher = Cipher.getInstance(_cipher);
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "unsupported algorithm in algorithm/mode/padding: "
+ _cipher, e);
throw new CryptoException(e);
} catch (NoSuchPaddingException e) {
Log.e(TAG, "unsupported padding in algorithm/mode/padding: "
+ _cipher, e);
throw new CryptoException(e);
}
try {
cipher.init(Cipher.ENCRYPT_MODE, coreKey);
} catch (InvalidKeyException e) {
Log.e(TAG, "invalid public core key", e);
throw new CryptoException(e);
}
try {
return cipher.doFinal(utf8bytes(in));
} catch (IllegalBlockSizeException e) {
Log.e(TAG, "public core key encryption(" + _cipher
+ ") => illegal block size for: " + in, e);
throw new CryptoException(e);
} catch (BadPaddingException e) {
Log.e(TAG, "public core key encryption(" + _cipher
+ ") => bad padding for: " + in, e);
throw new CryptoException(e);
}
}
private static boolean to_email(Coupon c) throws NoEmailProvidedException {
synchronized (mutex) {
ensure_init();
ensure_email();
JSONObject json = new JSONObject();
try {
json.put("type", "email");
json.put("uid", getEmail());
json.put("gid", gid);
json.put("uuid", c.uuid);
json.put("code", c.code);
} catch (JSONException e) {
Log.e(TAG, "failed to compose json payload for bind request", e);
return false;
}
URL url = make_url(coreUrl + "/1.0/coupon/" + c.uuid + "/bind");
if (url == null)
return false;
String payload;
try {
payload = base64(public_core_encrypt(json.toString()));
} catch (CryptoException e) {
Log.e(TAG, "failed to generate message for email bind", e);
return false;
}
release(c);
return post(url, payload) != null;
}
}
private static boolean to_wallet(Coupon c)
throws NoWalletInstalledException {
synchronized (mutex) {
Log.i(TAG, "sending to wallet coupon: " + c.uuid);
ensure_init();
ensure_wallet(); // TODO: uncomment
// extend the coupon lifetime so the wallet can actually bind it in
// *a-long-time*
fetch_coupon(c.uuid, COUPON_TO_WALLET_WAIT_LIFETIME);
release(c);
ctx.sendBroadcast(make_intent(c.uuid, c.code));
/*
* // ========================================================= //
* TODO: remove this once tests done JSONObject json = new
* JSONObject(); try { json.put("type", "wallet"); json.put("uid",
* "0cfJnG4uezEfUd2dGRYaWDJiX7IfNRMfum2mJRPwwJk="); json.put("gid",
* gid); json.put("uuid", c.uuid); json.put("code", c.code); } catch
* (JSONException e) { Log.e(TAG,
* "failed to compose json payload for bind request", e); return
* false; } URL url = make_url(coreUrl + "/1.0/coupon/" + c.uuid +
* "/bind"); if (url == null) return false; String payload; try {
* payload = base64(public_core_encrypt(json.toString())); } catch
* (CryptoException e) { Log.e(TAG,
* "failed to generate message for email bind", e); return false; }
* release(c); post(url, payload); // TODO: up to here
*/
// =========================================================
return true;
}
}
/**
* @return
*/
public static String getEmail() {
return email;
}
/**
* @param email
*/
public static void setEmail(String email) {
Log.i(TAG, "setting email to: " + email);
SharedPreferences sp = ctx.getSharedPreferences(SHARED_PREFERENCES,
Context.MODE_PRIVATE);
if (!sp.edit().putString("email", email).commit()) {
Log.w(TAG, "failed to save email value in shared preferences");
}
Wakuwaku.email = email;
}
/**
* @return
*/
public static boolean hasEmail() {
return (email != null) && (!"".equals(email));
}
private static void ensure_email() throws NoEmailProvidedException {
if (!hasEmail()) {
Log.w(TAG, "email required but not provided!");
throw new NoEmailProvidedException();
}
}
private static void ensure_wallet() throws NoWalletInstalledException {
if (!walletInstalled()) {
Log.w(TAG, "wallet required but not installed!");
throw new NoWalletInstalledException();
}
}
private static void ensure_init() throws InitRuntimeException {
if (!init_done)
throw new InitRuntimeException();
}
/**
* @return the number of images currently loaded in memory
*/
public static int countImages() {
HashSet<URL> urls = new HashSet<URL>();
for (Coupon c : coupons) {
if (c.hasDrawableLoaded())
urls.add(c.url);
}
return urls.size();
}
public static class Helpers {
/**
* Folded down the different network setup combinations to these 3 ones.
*
* @author Johan Gall <johan.gall@waku-waku.ne.jp>
*
*/
public static enum RadioState {
WIFI, FAST_MOBILE, SLOW_MOBILE;
}
/**
* Will wait maximum *ms* to fetch a coupon (if necessary) and return
* it. Returns null if the operation is not successful (typically the
* request was denied or the communication was too slow).
*
* @param duration
* duration to wait (without the unit)
* @param unit
* unit for the duration we need to wait for
* @param coupons
* number of coupons to fetch if proven necessary, the method
* will return after the first coupon if fetched
* @return A coupon, or null in case of failure.
*/
public static Coupon awaitFetchAndTake(long duration, TimeUnit unit,
int coupons) {
Coupon c = take();
if (c != null)
return c;
fetch(coupons);
fetching_lock.lock();
try {
try {
fetch_done.await(duration, unit);
} catch (InterruptedException e) {
Log.e(TAG,
"interrupted while waiting for a fetch to be done",
e);
}
return take();
} finally {
fetching_lock.unlock();
}
}
/**
* Simple helper to know the radio state you might use to determine your
* coupon fetching policy.
*
* @return
*/
public static RadioState radioState() {
ConnectivityManager cm = (ConnectivityManager) ctx
.getSystemService(Context.CONNECTIVITY_SERVICE);
TelephonyManager tm = (TelephonyManager) ctx
.getSystemService(Context.TELEPHONY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
switch (activeNetwork.getType()) {
case (ConnectivityManager.TYPE_WIFI):
return RadioState.WIFI;
case (ConnectivityManager.TYPE_MOBILE): {
switch (tm.getNetworkType()) {
case (TelephonyManager.NETWORK_TYPE_LTE | TelephonyManager.NETWORK_TYPE_HSPAP):
return RadioState.FAST_MOBILE;
case (TelephonyManager.NETWORK_TYPE_EDGE | TelephonyManager.NETWORK_TYPE_GPRS):
return RadioState.SLOW_MOBILE;
default:
return RadioState.SLOW_MOBILE;
}
}
default:
return RadioState.SLOW_MOBILE;
}
}
}
static private String no_null(Object o) {
return (o != null ? o.toString() : "null");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment