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

    View Slide

  2. 2 @tsmith

    View Slide

  3. 3 @tsmith

    View Slide

  4. 4 @tsmith

    View Slide

  5. Integration Requirements
    • Get Note(s)
    • List Notes
    • Create/Update Notes
    • Delete Notes
    • Account Sync
    • Get Preferences
    5 @tsmith

    View Slide

  6. Android Components Needed
    1. Intents
    2. Content Provider
    3. Account Manager
    4. Sync Adapter
    5. Inter Process Communication
    6. Permissions
    6 @tsmith

    View Slide

  7. Intents
    Overview
    • Simple message between two components
    • Bundle - key/value data
    • Limited payload size (1MB)
    • Inefficient for batch operations
    • Defined Action
    7 @tsmith

    View Slide

  8. Intents
    Standard Actions
    • ACTION_SEND
    • ACTION_SEND_MULTIPLE
    • ACTION_VIEW
    • ACTION_EDIT
    8 @tsmith

    View Slide

  9. 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

    View Slide

  10. Editing Intent
    Receiver







    10 @tsmith

    View Slide

  11. 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

    View Slide

  12. 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

    View Slide

  13. Intents
    Edit Caveats
    • Use copy of file
    • Don’t rely on setResult()
    • Use a ContentObserver
    • Check for file modified
    13 @tsmith

    View Slide

  14. Intents
    Custom Actions
    com.example.action.note.*
    list - view - new - create
    edit - update - delete
    14 @tsmith

    View Slide

  15. 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

    View Slide

  16. Content Provider
    A well defined database access layer
    • SQLite
    • Interact with large volumes of data
    • Authority
    16 @tsmith

    View Slide

  17. Content Provider
    URI
    content:// com.example / users / 1
    Scheme Authority Data Id
    17 @tsmith

    View Slide

  18. 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

    View Slide

  19. 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

    View Slide

  20. 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

    View Slide

  21. 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

    View Slide

  22. Account Manager
    • Accessible Account
    • Authentication Token
    • Consistency
    22 @tsmith

    View Slide

  23. Providing your own Accounts
    • AbstractAccountAuthenticator
    • AccountAuthenticatorActivity
    23 @tsmith

    View Slide

  24. 24 @tsmith

    View Slide

  25. 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

    View Slide

  26. 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

    View Slide

  27. 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

    View Slide

  28. 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

    View Slide

  29. 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

    View Slide

  30. Sync Adapter
    • Sync App and Web Service
    • User visible syncing
    • Network/battery optimized
    • Provided to third party apps
    • Schedule and GCM
    30 @tsmith

    View Slide

  31. 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 = getImagesFromServer(authToken);
    updateImagesOnDisk(provider, images);
    }
    }
    31 @tsmith

    View Slide

  32. 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

    View Slide

  33. Sync Adapter
    res/xml/sync_adapter.xml
    android:contentAuthority="com.example.images.provider"
    android:accountType="com.example.sync_example"
    android:userVisible="true"
    android:allowParallelSyncs="false"
    android:isAlwaysSyncable="false"
    android:supportsUploading="true"/>
    33 @tsmith

    View Slide

  34. Sync Adapter
    AndroidManifest.xml
    android:name=".syncadapter.ImagesSyncService"
    android:exported="true">



    android:name="android.content.SyncAdapter"
    android:resource="@xml/sync_adapter" />

    34 @tsmith

    View Slide

  35. Sync Adapter
    Sync Every Hour
    int interval = 3600;
    ContentResolver.addPeriodicSync(account,
    AppContract.AUTHORITY,
    new Bundle(),
    interval);
    35 @tsmith

    View Slide

  36. Sync Adapter
    Jitter
    int jitteredInterval = 3600 + new Random().nextInt(300);
    ContentResolver.addPeriodicSync(account,
    AppContract.AUTHORITY,
    new Bundle(),
    jitteredInterval);
    36 @tsmith

    View Slide

  37. 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

    View Slide

  38. Binding Services
    Create AIDL
    interface RemoteMessageService {
    void passMessage(String "message");
    }
    38 @tsmith

    View Slide

  39. Binding Services
    Implement AIDL
    public class MessageBinder implements RemoteMessageService.Stub() {
    public void passMessage(String message) {
    Log.d(TAG, message);
    }
    }
    39 @tsmith

    View Slide

  40. Binding Services
    Expose Interface
    public class RemoteService extends Service {
    @Override
    public IBinder onBind(Intent intent) {
    return new MessageBinder;
    }
    }
    40 @tsmith

    View Slide

  41. 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

    View Slide

  42. Permissions
    • Inform the User
    • Mitigate Exploits
    42 @tsmith

    View Slide

  43. Permissions
    Levels
    • Normal
    • Dangerous
    • Signature
    • SignatureOrSystem
    43 @tsmith

    View Slide

  44. Permissions
    Custom
    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

    View Slide

  45. 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

    View Slide

  46. Permissions
    Enforcing via XML
    android:permissionGroup="com.example.permission-group.MYAPP_DATA"
    android:protectionLevel="dangerous" />
    android:permissionGroup="com.example.permission-group.MYAPP_DATA"
    android:protectionLevel="dangerous" />
    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

    View Slide

  47. 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

    View Slide

  48. 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

    View Slide

  49. Resources
    Example App
    Dashclock for Evernote
    Android Developer Docs on AIDL
    Write your own Android Authenticator
    49 @tsmith

    View Slide

  50. Thanks!
    Ty Smith
    @tsmith
    50 @tsmith

    View Slide