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

App To Ap: Designing Local APIs On Android

7a3baf2e1158e358885cdf7e89b9aa55?s=47 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!

7a3baf2e1158e358885cdf7e89b9aa55?s=128

Ty Smith

November 06, 2015
Tweet

Transcript

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

    Android Engineer at Twitter 1 @tsmith
  2. 2 @tsmith

  3. 3 @tsmith

  4. 4 @tsmith

  5. Integration Requirements • Get Note(s) • List Notes • Create/Update

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

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

    - key/value data • Limited payload size (1MB) • Inefficient for batch operations • Defined Action 7 @tsmith
  8. Intents Standard Actions • ACTION_SEND • ACTION_SEND_MULTIPLE • ACTION_VIEW •

    ACTION_EDIT 8 @tsmith
  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
  10. 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
  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
  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
  13. Intents Edit Caveats • Use copy of file • Don’t

    rely on setResult() • Use a ContentObserver • Check for file modified 13 @tsmith
  14. Intents Custom Actions com.example.action.note.* list - view - new -

    create edit - update - delete 14 @tsmith
  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
  16. Content Provider A well defined database access layer • SQLite

    • Interact with large volumes of data • Authority 16 @tsmith
  17. Content Provider URI content:// com.example / users / 1 Scheme

    Authority Data Id 17 @tsmith
  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
  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
  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
  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
  22. Account Manager • Accessible Account • Authentication Token • Consistency

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

  24. 24 @tsmith

  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. Sync Adapter • Sync App and Web Service • User

    visible syncing • Network/battery optimized • Provided to third party apps • Schedule and GCM 30 @tsmith
  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> images = getImagesFromServer(authToken); updateImagesOnDisk(provider, images); } } 31 @tsmith
  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
  33. Sync Adapter res/xml/sync_adapter.xml <sync-adapter xmlns:android="http://schemas.android.com/apk/res/android" 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
  34. 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
  35. Sync Adapter Sync Every Hour int interval = 3600; ContentResolver.addPeriodicSync(account,

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

    ContentResolver.addPeriodicSync(account, AppContract.AUTHORITY, new Bundle(), jitteredInterval); 36 @tsmith
  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
  38. Binding Services Create AIDL interface RemoteMessageService { void passMessage(String "message");

    } 38 @tsmith
  39. Binding Services Implement AIDL public class MessageBinder implements RemoteMessageService.Stub() {

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

    @Override public IBinder onBind(Intent intent) { return new MessageBinder; } } 40 @tsmith
  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
  42. Permissions • Inform the User • Mitigate Exploits 42 @tsmith

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

    43 @tsmith
  44. 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
  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
  46. 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
  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
  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
  49. Resources Example App Dashclock for Evernote Android Developer Docs on

    AIDL Write your own Android Authenticator 49 @tsmith
  50. Thanks! Ty Smith @tsmith 50 @tsmith