$30 off During Our Annual Pro Sale. View Details »

Deep Android Integrations

Ty Smith
March 18, 2017

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!

Ty Smith

March 18, 2017
Tweet

More Decks by Ty Smith

Other Decks in Programming

Transcript

  1. 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
  2. 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
  3. 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
  4. Single Sign-On • Using URIs • Using intents with custom

    actions • Using the Android Account Manager 15 @tsmith
  5. 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
  6. 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
  7. Integration Requirements • Get Note(s) • List Notes • Create/Update

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

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

    key/value data • Limited payload size (1MB) • Inefficient for batch operations • Defined Action 22 @tsmith
  10. 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
  11. 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
  12. 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
  13. 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
  14. Intents Edit Caveats • Use copy of file • Don’t

    rely on setResult() • Use a ContentObserver • Check for file modified 28 @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); } 30 @tsmith
  16. Content Provider A well defined database access layer • SQLite

    • Interact with large volumes of data • Authority 31 @tsmith
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. Sync Adapter • Sync App and Web Service • User

    visible syncing • Network/battery optimized • Provided to third party apps • Schedule and GCM 45 @tsmith
  27. 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
  28. 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
  29. 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
  30. Sync Adapter Sync Every Hour int interval = 3600; ContentResolver.addPeriodicSync(account,

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

    ContentResolver.addPeriodicSync(account, AppContract.AUTHORITY, new Bundle(), jitteredInterval); 51 @tsmith
  32. 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
  33. Binding Services Implement AIDL public class MessageBinder implements RemoteMessageService.Stub() {

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

    @Override public IBinder onBind(Intent intent) { return new MessageBinder; } } 55 @tsmith
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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