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

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!

Ty Smith

March 18, 2017
Tweet

More Decks by Ty Smith

Other Decks in Programming

Transcript

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

    View Slide

  2. 2 @tsmith

    View Slide

  3. 3 @tsmith

    View Slide

  4. Deeplinks
    4 @tsmith

    View Slide

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

    View Slide

  6. Android Manifest
    android:name=".RideRequestActivity">







    6 @tsmith

    View Slide

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

    View Slide

  8. Universal (Deferred) deeplinks
    8 @tsmith

    View Slide

  9. 9 @tsmith

    View Slide

  10. App linking
    android:name=".RideRequestActivity">







    10 @tsmith

    View Slide

  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

    View Slide

  12. 12 @tsmith

    View Slide

  13. Three Legged Auth
    13 @tsmith

    View Slide

  14. Single Sign-On
    14 @tsmith

    View Slide

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

    View Slide

  16. Single Sign-On (URIs)
    16 @tsmith

    View Slide

  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

    View Slide

  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

    View Slide

  19. 19 @tsmith

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  25. Editing Intent
    Receiver







    25 @tsmith

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  39. 39 @tsmith

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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 = getImagesFromServer(authToken);
    updateImagesOnDisk(provider, images);
    }
    }
    46 @tsmith

    View Slide

  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

    View Slide

  48. Sync Adapter
    res/xml/sync_adapter.xml
    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

    View Slide

  49. Sync Adapter
    AndroidManifest.xml
    android:name=".syncadapter.ImagesSyncService"
    android:exported="true">



    android:name="android.content.SyncAdapter"
    android:resource="@xml/sync_adapter" />

    49 @tsmith

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  57. Permissions
    • Inform the User
    • Mitigate Exploits
    57 @tsmith

    View Slide

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

    View Slide

  59. Permissions
    Custom
    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

    View Slide

  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

    View Slide

  61. Permissions
    Enforcing via XML
    android:permissionGroup="com.example.permission-group.MYAPP_DATA"
    android:protectionLevel="dangerous" />
    android:permissionGroup="com.example.permission-group.MYAPP_DATA"
    android:protectionLevel="dangerous" />
    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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide