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

App To Ap: Designing Local APIs On Android

Ty Smith
November 06, 2015

App To Ap: Designing Local APIs On Android

Designing an elegant interface and local APIs for developers to communicate with your Android app is crucial for building a platform for your product: in this talk, you’ll find out how to allow third party developers to seamlessly interact with your users’ local data and shortcut more expensive server operations. Topics will include creating and exporting your content provider, constructing a well defined Intent interface, use deep links, and bind services for programmatic communication. Ty will walk you through how to create a well-defined interface in your app – if that’s something you want to know, don’t miss it!

Ty Smith

November 06, 2015
Tweet

More Decks by Ty Smith

Other Decks in Programming

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. AbstractAccountAuthenticat or 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. AbstractAccountAuthenticat or 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. AbstractAccountAuthenticat or 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. AbstractAccountAuthenticat or 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. AccountAuthenticatorActivit y 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