Slide 1

Slide 1 text

Deep Android Integrations Ty Smith Mobile Tech Lead at Uber 1 @tsmith

Slide 2

Slide 2 text

2 @tsmith

Slide 3

Slide 3 text

3 @tsmith

Slide 4

Slide 4 text

Deeplinks 4 @tsmith

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

Android Manifest 6 @tsmith

Slide 7

Slide 7 text

Universal links https:// m.uber.com/ul/rideRequest ?pickup[latitude]=23.3219383&pickup[longitude]=121.23123422 Scheme Authority query 7 @tsmith

Slide 8

Slide 8 text

Universal (Deferred) deeplinks 8 @tsmith

Slide 9

Slide 9 text

9 @tsmith

Slide 10

Slide 10 text

App linking 10 @tsmith

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

12 @tsmith

Slide 13

Slide 13 text

Three Legged Auth 13 @tsmith

Slide 14

Slide 14 text

Single Sign-On 14 @tsmith

Slide 15

Slide 15 text

Single Sign-On • Using URIs • Using intents with custom actions • Using the Android Account Manager 15 @tsmith

Slide 16

Slide 16 text

Single Sign-On (URIs) 16 @tsmith

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

19 @tsmith

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

Intents Standard Actions • ACTION_SEND • ACTION_SEND_MULTIPLE • ACTION_VIEW • ACTION_EDIT 23 @tsmith

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Editing Intent Receiver 25 @tsmith

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

Account Manager • Accessible Account • Authentication Token • Consistency 37 @tsmith

Slide 38

Slide 38 text

Providing your own Accounts • AbstractAccountAuthenticator • AccountAuthenticatorActivity 38 @tsmith

Slide 39

Slide 39 text

39 @tsmith

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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); } } 46 @tsmith

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

Sync Adapter res/xml/sync_adapter.xml 48 @tsmith

Slide 49

Slide 49 text

Sync Adapter AndroidManifest.xml 49 @tsmith

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

Permissions • Inform the User • Mitigate Exploits 57 @tsmith

Slide 58

Slide 58 text

Permissions Levels • Normal • Dangerous • Signature • SignatureOrSystem 58 @tsmith

Slide 59

Slide 59 text

Permissions Custom • 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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

Permissions Enforcing via XML 61 @tsmith

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

Thanks! Ty Smith @tsmith developers.uber.com 64 @tsmith