Upgrade to Pro — share decks privately, control downloads, hide ads and more …

老爹必備的奶瓶與鍵盤之道

 老爹必備的奶瓶與鍵盤之道

本議程分享某位新手爸爸在一個月內一手拿奶瓶餵小孩一手敲鍵盤寫程式參加比賽贏得尿布錢除了愛恨交織更是內牛滿面的故事。透過現成好用的 open source library,開發手機應用程式不必辛苦重新造輪,也不必苦苦等著使用者回報 debug,更可輕鬆加入付費機制突破天龍國的封鎖向鄉民收錢。

Shaka Huang

October 27, 2012
Tweet

More Decks by Shaka Huang

Other Decks in Programming

Transcript

  1. Experience • was @ BenQisda • Mobile Software Engineer/Manager (UI)

    • BREW / Android • now @ SmarTapper & Mocharabbit • .. coding (&) monkey 8
  2. Pai Pai (ഥഥ) • Take picture / draw / frame

    / note • DEMO @ OSDC.tw (4/15) • Launch @ Google Play Store (9/1) • In-app purchase: $1.99USD • LINE Camera (4/12) http://pkg.to/com.corner23.android.fourshots 13
  3. Pai Pai (ഥഥ) • Take picture / draw / frame

    / note • DEMO @ OSDC.tw (4/15) • Launch @ Google Play Store (9/1) • In-app purchase: $1.99USD • LINE Camera (4/12) http://pkg.to/com.corner23.android.fourshots 14
  4. In-app purchase stats • 1000 download / 1 purchase •

    Full: 46 (1.99) • Donation: 7 • Juice: 1 (0.99) • Coffee: 5 (1.99) • Beer: 1 (2.99) 16
  5. 25

  6. 26

  7. 28

  8. 30

  9. 31

  10. 32

  11. Templates http://goo.gl/NT5uX • 3.5 inches — 640 × 960 pixels

    • 4.65 inches — 1280 × 720 pixels • 5.3 inches — 1280 × 800 pixels • 7 inches — 1024 × 600 pixels • 7 inches — 1280 × 800 pixels • 7.7 inches — 1280 × 800 pixels • 8.9 inches — 1280 × 800 pixels • 9.7 inches — 1024 × 768 and 2048 × 1536 pixels • 10.1 inches — 1280 × 800 pixels 34
  12. UI Library • ActionBarSherlock (basic UI) • Android Color Picker

    (pencil color) • NotificationCompact2 (upload notification) • ViewPagerIndicator (frame/effect/ background chooser) • HoloEverywhere (theme) 36
  13. UI Library • ActionBarSherlock ୞༻෦෼ • Android Color Picker (pencil

    color) • NotificationCompact2 (upload notification) • ViewPagerIndicator (frame/effect/ background chooser) • HoloEverywhere (theme) ໦ࡐࣗવ෩ 37
  14. Usage AmbilWarnaDialog dialog = new AmbilWarnaDialog(this, initialColor, new OnAmbilWarnaListener() {

    @Override public void onOk(AmbilWarnaDialog dialog, int color) { // color is the color selected by the user. } @Override public void onCancel(AmbilWarnaDialog dialog) { // cancel was selected by the user } }); dialog.show(); 42
  15. NotificationCompact2 • Ease the pain of Notification & Notification.Builder •

    https://github.com/JakeWharton/ NotificationCompat2 43
  16. Intent intent = new Intent(); PendingIntent pi = PendingIntent.getActivity(this, 0,

    intent, 0); mNotification = new NotificationCompat2.Builder(this) " " " " .setAutoCancel(true) " " " " .setContentIntent(pi) " " " " .setContentText(“This is content”) .setContentTitle(“This is title”) .setPriority(NotificationCompat2.PRIORITY_DEFAULT) " " " " .setSmallIcon(R.drawable.icon) " " " " .setWhen(System.currentTimeMillis()) " " " " .build(); notifyManager.notify(NOTIFICATION_ID, mNotification); Usage 44
  17. Intent intent = new Intent(); PendingIntent pi = PendingIntent.getActivity(this, 0,

    intent, 0); mNotification = new NotificationCompat2.Builder(this) " " " " .setAutoCancel(true) " " " " .setContentIntent(pi) " " " " .setContentText(“This is content”) .setContentTitle(“This is title”) .setPriority(NotificationCompat2.PRIORITY_DEFAULT) " " " " .setSmallIcon(R.drawable.icon) " " " " .setWhen(System.currentTimeMillis()) " " " " .build(); notifyManager.notify(NOTIFICATION_ID, mNotification); Usage 45
  18. Usage (java) //Set the pager with an adapter ViewPager pager

    = (ViewPager)findViewById(R.id.pager); pager.setAdapter(new TestAdapter(getSupportFragmentManager())); //Bind the title indicator to the adapter TitlePageIndicator titleIndicator = (TitlePageIndicator)findViewById(R.id.titles); titleIndicator.setViewPager(pager); titleIndicator.setOnPageChangeListener(mPageChangeListener); 48
  19. Recap • Paper prototyping • Use of libraries • ActionBarSherlock

    • ViewPageIndicator • NotificationCompact2 • Android Color Picker 49
  20. 53

  21. 59

  22. Take a guess • How many combinations can be made

    out of “Manufacturer + Model + API level” in one single application ? • Reference: Battery HD http://pkg.to/ch.smalltech.battery.free 62
  23. 64

  24. 69

  25. import org.acra.*; import org.acra.annotation.*; @ReportsCrashes(formKey = “dGVacG0ydVHnaNHjRjVTUTEtb3FPWGc6MQ”) public class MyApplication

    extends Application { @Override public void onCreate() { // The following line triggers the initialization of ACRA ACRA.init(this); super.onCreate(); } } <manifest ...> <application ... android:name=”MyApplication”> ... </application> <uses-permission android:name=”android.permission.INTERNET” /> </manifest> Modify Codes AndroidManifest.xml Extends Application class 74
  26. import org.acra.*; import org.acra.annotation.*; @ReportsCrashes(formKey = “dGVacG0ydVHnaNHjRjVTUTEtb3FPWGc6MQ”) public class MyApplication

    extends Application { @Override public void onCreate() { // The following line triggers the initialization of ACRA ACRA.init(this); super.onCreate(); } } <manifest ...> <application ... android:name=”MyApplication”> ... </application> <uses-permission android:name=”android.permission.INTERNET” /> </manifest> Modify Codes AndroidManifest.xml Extends Application class 75
  27. Tips for ACRA • Spreadsheets for different version • Turn

    off for debugging • Notification + User preference 76
  28. private static final Signature RELEASE_SIG = new Signature("<YOUR SIGNATURE HERE>");

    public static boolean isRelease(Context context) { try { PackageInfo info = context.getPackageManager().getPackageInfo( context.getPackageName(), PackageManager.GET_SIGNATURES); for (Signature sig : info.signatures) { if (sig.equals(RELEASE_SIG)) { return true; } } } catch (Exception e) { return false; } return false; } Detecting release builds 78
  29. try { " // The following line triggers the initialization

    of ACRA " ACRA.init(this); " ACRAConfiguration conf = ACRA.getConfig(); " conf.setResToastText(R.string.str_crash_report); " conf.setMode(ReportingInteractionMode.TOAST); " conf.setForceCloseDialogAfterToast(false); conf.setExcludeMatchingSharedPreferencesKeys(new String[] {"^facebook"}); " ACRA.setConfig(conf); } catch (RuntimeException e) { " e.printStackTrace(); } catch (ACRAConfigurationException e) { " e.printStackTrace(); } Report notification 79
  30. WAIT ! • “ Real world is dangerous “ •

    I want my app running on more devices without crashing ! 82
  31. Need more testers • Samsung Apps • FREE ! •

    Professional QA with phones & tablets • Video feedback • #1 market share in most countries 83
  32. Recap • Developer phone test • Crash report library •

    Online crash report / analysis service • Store w/ testers • Samsung Apps 84
  33. 92

  34. 94

  35. Overview • Google Play service • 30% of the sale

    price • API level 4+ (1.6+) • Types • In-app product (Managed/unmanaged) • Subscription 95
  36. Register • Register developer in Google Play • Register Google

    Wallet for buyers • No more Google Adsense ! 97
  37. 107

  38. Android Billing Library • Full IAB implementation • Obfuscated purchases

    database • Unit test https://github.com/robotmedia/AndroidBillingLibrary 112
  39. <manifest> <uses-permission android:name="com.android.vending.BILLING" /> <service android:name="net.robotmedia.billing.BillingService" /> <receiver android:name="net.robotmedia.billing.BillingReceiver"> <intent-filter>

    <action android:name="com.android.vending.billing.IN_APP_NOTIFY" /> <action android:name="com.android.vending.billing.RESPONSE_CODE" /> <action android:name="com.android.vending.billing.PURCHASE_STATE_CHANGED " /> </intent-filter> </receiver> </manifest> AndroidManifest.xml 113
  40. Set configuration @Override public void onCreate() { super.onCreate(); " BillingController.setDebug(isDebug(this));

    " BillingController.setConfiguration(new BillingController.IConfiguration() { " " public byte[] getObfuscationSalt() { " " " return new byte[] {-127, -96, -88, 77, 127, 115, 41, -90, -116, -41, 66, -53, 122, -110, 1, 73, 57, 110, 48, -116}; " " } " " public String getPublicKey() { " " " return “<YOUR PUBLIC KEY>”; " } }); } 114
  41. mBillingObserver = new AbstractBillingObserver(this) { public void onBillingChecked(boolean supported) {

    } public void onPurchaseStateChanged(String itemId, PurchaseState state) { } public void onRequestPurchaseResponse(String itemId, ResponseCode response) { } public void onSubscriptionChecked(boolean supported) { } }; Create observer 115
  42. Check billing support @Override public void onCreate(Bundle savedInstanceState) { //

    ... BillingController.registerObserver(mBillingObserver); BillingController.checkBillingSupported(this); // ... } protected void onBillingChecked(boolean supported) { if (supported) { // continue } else { showDialog(DIALOG_BILLING_NOT_SUPPORTED_ID); } } 116
  43. Restore transactions private void restoreTransactions() { if (!mBillingObserver.isTransactionsRestored()) { BillingController.restoreTransactions(this);

    Toast.makeText(this, R.string.restoring_transactions, Toast.LENGTH_LONG).show(); } } protected void onBillingChecked(boolean supported) { if (supported) { restoreTransactions(); } ... } 118
  44. Requests • MarketBillingService interface • (IMarketBillingService.aidl) • Requests sent by

    single IPC method (sendBillingRequest()) of the interface • Request parameters are sent as Bundle 125
  45. Binding try { boolean bindResult = mContext.bindService( new Intent("com.android.vending.billing.MarketBillingService.BIND"), this,

    Context.BIND_AUTO_CREATE); if (bindResult) { Log.i(TAG, "Service bind successful."); } else { Log.e(TAG, "Could not bind to the MarketBillingService."); } } catch (SecurityException e) { Log.e(TAG, "Security exception: " + e); } public void onServiceConnected(ComponentName name, IBinder service) { Log.i(TAG, "MarketBillingService connected."); mService = IMarketBillingService.Stub.asInterface(service); } 126
  46. protected Bundle makeRequestBundle(String method) { Bundle request = new Bundle();

    request.putString(BILLING_REQUEST, method); request.putInt(API_VERSION, 1); request.putString(PACKAGE_NAME, getPackageName()); return request; } Request bundle 127
  47. Make request Bundle request = makeRequestBundle("REQUEST_PURCHASE"); request.putString(ITEM_ID, mProductId); // Request

    is for a standard in-app product request.putString(ITEM_TYPE, "inapp"); // Note that the developer payload is optional. if (mDeveloperPayload != null) { request.putString(DEVELOPER_PAYLOAD, mDeveloperPayload); } Bundle response = mService.sendBillingRequest(request); 128
  48. Synchronous response • RESPONSE_CODE • status of request • PURCHASE_INTENT

    • PendingIntent to launch checkout UI • REQUEST_ID • unique request identifier for the request 130
  49. Receiving asynchronous responses @Override public void onReceive(Context context, Intent intent)

    { String action = intent.getAction(); if (ACTION_PURCHASE_STATE_CHANGED.equals(action)) { String signedData = intent.getStringExtra(INAPP_SIGNED_DATA); String signature = intent.getStringExtra(INAPP_SIGNATURE); // Do something with the signedData and the signature. } else if (ACTION_NOTIFY.equals(action)) { String notifyId = intent.getStringExtra(NOTIFICATION_ID); // Do something with the notifyId. } else if (ACTION_RESPONSE_CODE.equals(action)) { long requestId = intent.getLongExtra(INAPP_REQUEST_ID, -1); int responseCodeIndex = intent.getIntExtra(INAPP_RESPONSE_CODE, ResponseCode.RESULT_ERROR.ordinal()); // Do something with the requestId and the responseCodeIndex. } else { Log.w(TAG, "unexpected action: " + action); } } 132
  50. Check billing supported • Return: RESPONSE_CODE (Sync) • RESULT_OK /

    RESULT_BILLING_UNAVAILABLE / RESULT_ERROR / RESULT_DEVELOPER_ERROR 135
  51. Request purchase Bundle request = makeRequestBundle("REQUEST_PURCHASE"); request.putString(ITEM_ID, mProductId); // Request

    is for a standard in-app product request.putString(ITEM_TYPE, "inapp"); // Note that the developer payload is optional. if (mDeveloperPayload != null) { request.putString(DEVELOPER_PAYLOAD, mDeveloperPayload); } Bundle response = mService.sendBillingRequest(request); 136
  52. JSON of return info { "nonce" : 1836535032137741465, "orders" :

    [{ "notificationId" : "android.test.purchased", "orderId" : "transactionId.android.test.purchased", "packageName" : "com.example.dungeons", "productId" : "android.test.purchased", "developerPayload" : "bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ", "purchaseTime" : 1290114783411, "purchaseState" : 0, "purchaseToken" : "rojeslcdyyiapnqcynkjyyjh" }] } 140