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

Building First Class Android SDKs - Øredev 2015

Ty Smith
November 04, 2015

Building First Class Android SDKs - Øredev 2015

Fabric, formerly Crashlytics, is well-known for its focus on SDK quality, and has been deployed on billions of devices. In this session, attendees will learn the skills to develop and distribute SDKs for Android. We’ll cover an overview of Fabric, deep dive into technical decisions we made, and present the learnings on developing an SDK for stability, testability, performance, overall footprint size, and, most importantly, exceptional ease of implementation. Over the course of the session, we'll uncover and explain many of the challenges we encountered when building SDKs at Twitter. Topics include device feature detection, supporting multiple application types (from Widgets to Services to Foreground GUI applications), API design, deploying artifacts, and coding patterns to support developer customization. We'll conclude with advanced topics, from less-known but very useful tricks to minimizing impact on application start-up time to reducing memory footprint and persistent CPU use.

**Now using Deckset!**

Ty Smith

November 04, 2015
Tweet

More Decks by Ty Smith

Other Decks in Technology

Transcript

  1. App to App Designing Local APIs on Android Ty Smith

    Android Engineer at Twitter 1 @tsmith
  2. Integration Requirements • Get Note(s) • List Notes • Create/Update

    Notes • Delete Notes • Account Sync • Get Preferences 5 @tsmith
  3. Android Components Needed 1. Intents 2. Content Provider 3. Account

    Manager 4. Sync Adapter 5. Inter Process Communication 6. Permissions 6 @tsmith
  4. Intents Overview • Simple message between two components • Bundle

    - key/value data • Limited payload size (1MB) • Inefficient for batch operations • Defined Action 7 @tsmith
  5. Editing Intent Sender public void editImage(String mimeType, Uri imageUri) {

    Intent intent = new Intent(Intent.ACTION_EDIT); intent.setType(mimeType); intent.setData(imageUri); startActivityForResult(intent, RESULT_CODE); } 9 @tsmith
  6. Editing Intent Receiver <activity android:name=".ui.IntentActivity" > <intent-filter> <action android:name="android.intent.action.ACTION_EDIT" />

    <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="image/*" /> </intent-filter> </activity> 10 @tsmith
  7. Editing Intent Receiver @Override protected void onNewIntent(Intent intent) { switch(intent.getAction())

    { case Intent.ACTION_EDIT: if (intent.getType().startsWith("image/")) { editImage(intent.getData()); setResult(RESULT_OK, intent); finish(); } } } 11 @tsmith
  8. Editing Intent Sender @Override protected void onActivityResult(int requestCode, int resultCode,

    Intent data) { if (requestCode = RESULT_CODE && RESULT_OK == resultCode) { Uri imageUri = data.getData(); //Update UI with new image } } 12 @tsmith
  9. Intents Edit Caveats • Use copy of file • Don’t

    rely on setResult() • Use a ContentObserver • Check for file modified 13 @tsmith
  10. Intents Custom Actions public void newNoteWithContent(Uri image) { Intent intent

    = new Intent(); intent.setAction(NEW_NOTE); intent.putExtra(Intent.EXTRA_TITLE, "LET ME EXPLAIN YOU INTENTS"); intent.putExtra(Intent.EXTRA_TEXT, "¯\_()_/¯"); intent.setData(image); startActivityForResult(intent); } 15 @tsmith
  11. Content Provider A well defined database access layer • SQLite

    • Interact with large volumes of data • Authority 16 @tsmith
  12. Content Provider Query public Cursor query(Uri uri, String[] projection, String

    selection, String[] selectionArgs, String sortOrder) { SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); switch (URIMatcher.match(uri)) { USERS_LIST: queryBuilder.setTables(MyDBHandler.TABLE_USERS); } Cursor cursor = queryBuilder.query(myDB.getReadableDatabase(), projection, selection, selectionArgs, null, null, sortOrder); cursor.setNotificationUri(getContext().getContentResolver(), uri); return cursor; } 18 @tsmith
  13. Content Resolver Query Uri uri = Uri.parse("content://com.example/users"); String[] projection =

    new String[]{"username", “email”}; String selection = “name LIKE ?”; String[] args = new String[]{"Ty"}; String sort = “email ASC”; getContentResolver().query(uri, projection, selection, args, sort); 19 @tsmith
  14. Content Provider Serving Files @Override public ParcelFileDescriptor openFile(Uri uri, String

    mode) throws FileNotFoundException { File path = new File(getContext().getCacheDir(), uri.getEncodedPath()); int imode = 0; if (mode.contains("w")) { imode |= ParcelFileDescriptor.MODE_WRITE_ONLY; if (!path.exists()) { try { path.createNewFile(); //TODO: Handle IOException } } if (mode.contains("r")) imode |= ParcelFileDescriptor.MODE_READ_ONLY; if (mode.contains("+")) imode |= ParcelFileDescriptor.MODE_APPEND; return ParcelFileDescriptor.open(path, imode); } 20 @tsmith
  15. Content Resolver Getting Files ContentResolver resolver = getContentResolver(); URI imageUri

    = Uri.parse("content://com.example/images/1"); String mode = "rw+" ParcelFileDescriptor pfd = resolver.openFileDescriptor(imageUri, mode); FileDescriptor fileDescriptor = pfd.getFileDescriptor(); InputStream fileStream = new FileInputStream(fileDescriptor); //Magic here! 21 @tsmith
  16. AbstractAccountAuthenticator addAccount @Override public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String

    authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException { final Intent intent = new Intent(mContext, AuthenticatorActivity.class); intent.putExtra(AuthenticatorActivity.ARG_ACCOUNT_TYPE, accountType); intent.putExtra(AuthenticatorActivity.ARG_AUTH_TYPE, authTokenType); intent.putExtra(AuthenticatorActivity.ARG_IS_ADDING_NEW_ACCOUNT, true); intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); final Bundle bundle = new Bundle(); bundle.putParcelable(AccountManager.KEY_INTENT, intent); return bundle; } 25 @tsmith
  17. AbstractAccountAuthenticator getAuthToken @Override public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String

    authTokenType, Bundle options) throws NetworkErrorException { final AccountManager am = AccountManager.get(mContext); String authToken = am.peekAuthToken(account, authTokenType); if (TextUtils.isEmpty(authToken)) { final String password = am.getPassword(account); if (password != null) { authToken = serverAuthenticate.userSignIn(account.name, password, authTokenType); } } ... 26 @tsmith
  18. AbstractAccountAuthenticator getAuthToken ... if (!TextUtils.isEmpty(authToken)) { final Bundle result =

    new Bundle(); result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); result.putString(AccountManager.KEY_AUTHTOKEN, authToken); return result; } ... 27 @tsmith
  19. AbstractAccountAuthenticator getAuthToken ... final Intent intent = new Intent(mContext, AuthenticatorActivity.class);

    intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); intent.putExtra(AuthenticatorActivity.ARG_ACCOUNT_TYPE, account.type); intent.putExtra(AuthenticatorActivity.ARG_AUTH_TYPE, authTokenType); final Bundle bundle = new Bundle(); bundle.putParcelable(AccountManager.KEY_INTENT, intent); return bundle; } 28 @tsmith
  20. AccountAuthenticatorActivity Success private void finishLogin(String accountName, String accountType, String password,

    String authToken, String authTokenType) { Account account = new Account(accountName, accountType); if (getIntent().getBooleanExtra(ARG_IS_ADDING_NEW_ACCOUNT, false)) { accountManager.addAccountExplicitly(account, password, null); } accountManager.setPassword(account, password); accountManager.setAuthToken(account, authTokenType, authToken); Intent intent = new Intent(); intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, userName); intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, ACCOUNT_TYPE); intent.putExtra(AccountManager.KEY_AUTHTOKEN, authToken); setAccountAuthenticatorResult(intent.getExtras()); setResult(RESULT_OK, intent); finish(); } 29 @tsmith
  21. Sync Adapter • Sync App and Web Service • User

    visible syncing • Network/battery optimized • Provided to third party apps • Schedule and GCM 30 @tsmith
  22. Sync Adapter public class ImagesSyncAdapter extends AbstractThreadedSyncAdapter { private final

    AccountManager accountManager; public TvShowsSyncAdapter(Context context, boolean autoInitialize) { super(context, autoInitialize); accountManager = AccountManager.get(context); } @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { String authToken = accountManager.blockingGetAuthToken(account, AccountGeneral.AUTHTOKEN_TYPE_FULL_ACCESS, true); List<Images> images = getImagesFromServer(authToken); updateImagesOnDisk(provider, images); } } 31 @tsmith
  23. Sync Adapter public class ImagesSyncService extends Service { private static

    final Object syncAdapterLock = new Object(); private static ImagesSyncAdapter syncAdapter = null; @Override public void onCreate() { synchronized (syncAdapterLock) { if (syncAdapter == null) syncAdapter = new ImagesSyncAdapter(getApplicationContext(), true); } } @Override public IBinder onBind(Intent intent) { return syncAdapter.getSyncAdapterBinder(); } } 32 @tsmith
  24. Sync Adapter AndroidManifest.xml <service android:name=".syncadapter.ImagesSyncService" android:exported="true"> <intent-filter> <action android:name="android.content.SyncAdapter" />

    </intent-filter> <meta-data android:name="android.content.SyncAdapter" android:resource="@xml/sync_adapter" /> </service> 34 @tsmith
  25. Sync Adapter Sync Every Hour int interval = 3600; ContentResolver.addPeriodicSync(account,

    AppContract.AUTHORITY, new Bundle(), interval); 35 @tsmith
  26. Sync Adapter Jitter int jitteredInterval = 3600 + new Random().nextInt(300);

    ContentResolver.addPeriodicSync(account, AppContract.AUTHORITY, new Bundle(), jitteredInterval); 36 @tsmith
  27. Binding Services for IPC • Android Interface Definition Language (AIDL)

    • Communicate between process or app • Directly call defined RPC methods • Requires consumer to contain interface 37 @tsmith
  28. Binding Services Implement AIDL public class MessageBinder implements RemoteMessageService.Stub() {

    public void passMessage(String message) { Log.d(TAG, message); } } 39 @tsmith
  29. Binding Services Expose Interface public class RemoteService extends Service {

    @Override public IBinder onBind(Intent intent) { return new MessageBinder; } } 40 @tsmith
  30. Binding Services Connecting private ServiceConnection connection = new ServiceConnection() {

    public void onServiceConnected(ComponentName className, IBinder service) { service = IRemoteService.Stub.asInterface(service); } public void onServiceDisconnected(ComponentName className) { service = null; } } public void onResume() { bindService(new Intent(this, RemoteMessageService.class), connection, Context.BIND_AUTO_CREATE); } public void onPause() { unbindService(connection); service = null; } 41 @tsmith
  31. Permissions Custom <permission android:name="com.example.perm.READ" android:label="@string/permission_label" android:description="@string/permission_description" android.protectionLevel="dangerous" android:permissionGroup="com.example.permission-group.MYAPP_DATA" /> •

    Can be used to restrict access to various services and components • Required to interact with app that declares it • Can be checked programatically or via Manifest. 44 @tsmith
  32. Permissions Enforcing Programitcally int canProcess = getContext().checkCallingPermission("com.example.perm.READ"); if (canProcess !=

    PERMISSION_GRANTED) { throw new SecurityException("Requires Custom Permission"); } • checkCallingOrSelfPermission() can leak permissions 45 @tsmith
  33. Permissions Enforcing via XML <permission android:name="com.example.perm.READ" android:permissionGroup="com.example.permission-group.MYAPP_DATA" android:protectionLevel="dangerous" /> <permission

    android:name="myapp.permission.WRITE" android:permissionGroup="com.example.permission-group.MYAPP_DATA" android:protectionLevel="dangerous" /> <provider android:name=".data.DataProvider" android:exported="true" android:authorities="com.example.data.DataProvider" android:readPermission="com.example.perm.READ" android:writePermission="com.example.perm.WRITE" /> 46 @tsmith
  34. Debugging Tips • Debugging app integrations is hard • Logging

    is your friend • Test with mock integrations • Communicate your data contracts • Seek User Feedback • Analytics and Crash Reporting 47 @tsmith
  35. Integration Requirements • Get Note(s) (Intent & Content Provider) •

    List Notes (Intent) • Create/Update Notes (Intent & Content Provider) • Delete Notes (Intent & Content Provider) • Account Sync (Account Manager & Sync Adapter) • Get Preferences (Content Provider and bound Services) 48 @tsmith
  36. Resources Example App Dashclock for Evernote Android Developer Docs on

    AIDL Write your own Android Authenticator 49 @tsmith