Last active
December 26, 2015 02:39
-
-
Save freakhill/7079774 to your computer and use it in GitHub Desktop.
Wakuwaku SDK client
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 <[email protected]> | |
* | |
*/ | |
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
Copyright (c) 2013 WAKU WAKU. All rights reserved. | |
Johan Gall <[email protected]> | |
*/ | |
/** | |
* 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 <[email protected]> | |
* @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 <[email protected]> | |
* @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 <[email protected]> | |
* | |
*/ | |
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