Deep Android Integrations

Deep Android Integrations

Designing an elegant interface for developers to communicate with your Android app is crucial for building a mobile platform. Ty has been building mobile-focused developer platforms at Evernote, Twitter, and now Uber. In this talk, he'll walk you through many of the best practices that he has accumulated and you’ll find out how to allow third party developers to seamlessly interact with your users’ local data to shortcut more expensive server operations.

Topics will include building single sign-on, surfacing your local databases, constructing well-defined Intent interfaces, using deep links, and binding 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

March 18, 2017
Tweet

Transcript

  1. Deep Android Integrations Ty Smith Mobile Tech Lead at Uber

    1 @tsmith
  2. 2 @tsmith

  3. 3 @tsmith

  4. Deeplinks 4 @tsmith

  5. URI uber:// rideRequest ?pickup[latitude]=37.7749&pickup[longitude]=-122.4194 Scheme Authority query 5 @tsmith

  6. Android Manifest <activity android:name=".RideRequestActivity"> <intent-filter> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT"/> <category

    android:name="android.intent.category.BROWSABLE"/> <data android:scheme="uber" android:authority="rideRequest"/> </intent-filter> </activity> 6 @tsmith
  7. Universal links https:// m.uber.com/ul/rideRequest ?pickup[latitude]=23.3219383&pickup[longitude]=121.23123422 Scheme Authority query 7 @tsmith

  8. Universal (Deferred) deeplinks 8 @tsmith

  9. 9 @tsmith

  10. App linking <activity android:name=".RideRequestActivity"> <intent-filter android:autoVerify="true"> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT"/>

    <category android:name="android.intent.category.BROWSABLE"/> <data android:scheme="uber" android:authority="rideRequest"/> </intent-filter> </activity> 10 @tsmith
  11. App linking $ keytool -list -v -keystore my-release-key.keystore # https://domain[:optional_port]/.well-known/assetlinks.json

    [{ "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.example", "sha256_cert_fingerprints": ["14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"] } }] 11 @tsmith
  12. 12 @tsmith

  13. Three Legged Auth 13 @tsmith

  14. Single Sign-On 14 @tsmith

  15. Single Sign-On • Using URIs • Using intents with custom

    actions • Using the Android Account Manager 15 @tsmith
  16. Single Sign-On (URIs) 16 @tsmith

  17. Single Sign-On Security Intent intent = new Intent(Intent.ACTION_VIEW); final Uri

    deepLinkUri = createSsoUri(); intent.setData(deepLinkUri); intent.setPackage("com.ubercab"); activity.startActivityForResult(intent, requestCode); 17 @tsmith
  18. Single Sign-On Security PackageInfo packageInfo = packageManager.getPackageInfo( packageName, PackageManager.GET_SIGNATURES); for

    (Signature signature : packageInfo.signatures) { String hashedSignature = Utility.sha1hash(signature.toByteArray()); if (!validAppSignatureHashes.contains(hashedSignature)) { //Invalid Signature } } //Valid signature 18 @tsmith
  19. 19 @tsmith

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

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

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

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

    ACTION_EDIT 23 @tsmith
  24. 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); } 24 @tsmith
  25. 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> 25 @tsmith
  26. 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(); } } } 26 @tsmith
  27. 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 } } 27 @tsmith
  28. Intents Edit Caveats • Use copy of file • Don’t

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

    create edit - update - delete 29 @tsmith
  30. 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); } 30 @tsmith
  31. Content Provider A well defined database access layer • SQLite

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

    Authority Data Id 32 @tsmith
  33. 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; } 33 @tsmith
  34. 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); 34 @tsmith
  35. 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); } 35 @tsmith
  36. 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! 36 @tsmith
  37. Account Manager • Accessible Account • Authentication Token • Consistency

    37 @tsmith
  38. Providing your own Accounts • AbstractAccountAuthenticator • AccountAuthenticatorActivity 38 @tsmith

  39. 39 @tsmith

  40. 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; } 40 @tsmith
  41. 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); } } ... 41 @tsmith
  42. 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; } ... 42 @tsmith
  43. 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; } 43 @tsmith
  44. 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(); } 44 @tsmith
  45. Sync Adapter • Sync App and Web Service • User

    visible syncing • Network/battery optimized • Provided to third party apps • Schedule and GCM 45 @tsmith
  46. 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); } } 46 @tsmith
  47. 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(); } } 47 @tsmith
  48. 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"/> 48 @tsmith
  49. 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> 49 @tsmith
  50. Sync Adapter Sync Every Hour int interval = 3600; ContentResolver.addPeriodicSync(account,

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

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

    • Communicate between process or app • Directly call defined RPC methods • Requires consumer to contain interface 52 @tsmith
  53. Binding Services Create AIDL interface RemoteMessageService { void passMessage(String "message");

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

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

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

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

    58 @tsmith
  59. 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. 59 @tsmith
  60. Permissions Enforcing programatically int canProcess = getContext().checkCallingPermission("com.example.perm.READ"); if (canProcess !=

    PERMISSION_GRANTED) { throw new SecurityException("Requires Custom Permission"); } • checkCallingOrSelfPermission() can leak permissions 60 @tsmith
  61. 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" /> 61 @tsmith
  62. 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) 62 @tsmith
  63. 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 63 @tsmith
  64. Thanks! Ty Smith @tsmith developers.uber.com 64 @tsmith