App to App: 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: this talk from Øredev Conference 2015 describes how to allow third party developers to seamlessly interact with your users’ local data and shortcut more expensive server operations. Topics include creating and exporting your Content Provider, constructing a well defined Intent interface, using deep links, and binding services for programmatic communication. Ty walks you through how to create a well-defined interface in your app.


Background (0:00)

My name is Ty, and I’m @tsmith. I am an Android engineer at Twitter, on the Fabric team. A couple years ago, I worked at Evernote building SDKs and partner integrations. At that time, Samsung approached us, wanting to build an integration between their S Note and Evernote product, using the Evernote back end as the primary data store for all of the S Notes on the device.

A traditional approach would have been to use an SDK that talks to a web service: it is flexible, and provides a guarantee that the third party code exists within the client app. However, every app on the device needs to bundle the same code, download the same data, duplicate it and make independent connections. We did not want the users to have to download the same notes twice.

So, we aimed to serve the data and the user experience from our application and create a platform out of the app. We decided to go with this approach (similar to others, e.g. Dropbox and Google Drive), providing files and account data to other apps that want to consume it.

There were requirements that we needed to meet:

  1. S Note content must be browsable from the main Evernote app.
  2. Notes must open from Evernote into S Note, be editable and return immediately to the Evernote client.
  3. S Note app must create and sync content asynchronously between the Evernote client.

Integration Requirements (2:10)

  • Get one or more notes, and provide to S Note.
  • An action that represented a UI list view.
  • Create a note from data, provide it or update an existing one.
  • Delete notes based on user’s input.
  • Read the Evernote account from the system, discover logged in stated and syncing preferences and kick off a sync between the Evernote app and our web service.
  • Expose preferences - S Note could show the appropriate user experience for the integration back into Evernote.

Android Components Needed (3:02)

  1. Intents (individual use cases, single notes)
  2. Content Provider (serves larger amounts of data)
  3. Account manager (exposing authentication information)
  4. Sync adapter (optimizing network connectivity)
  5. Inter Process Communication
  6. Permissions

Intents (3:43)

Intents are crucial for communication between two components. An intent can contain a bundle (a key/value store for serialized data), which allows one component to provide data to the recipient. Intents have limited payload sizes. We can use the setData method on an intent to provide a URI for the Content Provider to access that data. Intents are inefficient for multiple operations. The broadcasting system is running on the main thread - this can cause Android Not Responding errors (ANRs), where the main thread gets blocked for several seconds and it prompts the user to close it. One requirement when building the partner integrations while syncing larger amounts of data. By creating exposed actions on the intents and creating a consistent API, we can provide easy-to-use functionality for apps that interact directly on the device.

Intents - standard actions (5:05)

Include standard Android actions in your manifest; this will prompt the user with an intent picker.

ACTION_SEND allows you to send data be used in another app. ACTION_SEND_MULTIPLE is similar but it is designed for sending more than one piece of data at a time. ACTION_VIEW is similar to send, except it has different intentions where send is copying data out of your application, view’s intention, to say this other app can handle viewing (e.g. MIME type or specific data). ACTION_EDIT allows you to send a URI to be edited, and you receive a callback to your application. This allows you to have another application edit a file, and then return it to you. See an example below.

Editing intent - sender, receiver (6:10)

public void editImage(String mimeType, Uri imageUri) {
	Intent intent = new Intent(Intent.ACTION_EDIT);
	intent.setType(mimeType);
	intent.setData(imageUri);
	startActivityForResult(intent, RESULT_CODE);
}

When we create out intent, we set the action to edit directly and we specify a MIME type for the image, and the data URI. That is pulling from our Content Provider. Once we built our intent, we call the startActivityForResult: it will load the activity and expect it to provide a result status upon finishing.

<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>

On the receiver, we will implement an intent filter on our activity to handle the edit action for this specific image MIME type. This will allow our app to be picked by the user when editing images.

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

We need code to read the incoming intents and match against our parameters. We can start the editing process. As a receiver, we should be editing the URI given to use by the sender. We will set the results to OK and return the original get data intent (in this example).

@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
	}
}

On the sender side, we are notified in the onActivityResult method. If it was a successful operation, we can progress forward with the edited file.

Intents - edit caveats (7:19)

Do not send the original file to the recipient app without first making a copy. The recipient app may not return the result correctly (e.g. corrupt the file or make unintended changes, or the user may cancel the operation but the recipient app may have already persisted that to disk). When you come back you have your same URI but it has been edited. It will not be apparent to the user, in that experience where the UX broke down (app failing or recipient app failing?). By copying the file and sending the copy out to be edited, upon the onActivityResult, you can confirm it was a successful operation and swap the file.

Do not rely on the setResult() or onActivityResult. If the recipient app did not handle that correctly, your data could be in a limbo state and the user will think it was your app’s fault. Another strategy to watch for changes in the file is through a URI with a ContentObserver. If the user has kill activities, could be problematic. Also check for file modified when resuming your activity. If you have a similar process, you could create an MD5 of the file before you send it over. On returning, you could check if the file was modified and communicate that to the user for swapping them.

Intents - custom sctions (9:07)

On the Evernote app, we created a set of intent APIs, modeled after the standard CRUD. We use standard content types. For example, note, notebook and the caller can engage with expected APIs on data objects. If the list intent is used, we take the user to a UI limit that shows a list of the content type that they expect; if new was used, we open a composer. However, if create or update was called then we would update the specific object. We would expect all the data to be pre-filled in the bundle and return back to the calling application.

Content Provider (10:25)

public voic 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);
}

We set the action. We provide a title, text, and set the data URI in the image. The recipient note app would get the data and fill it into the composer (works for single data). For larger datasets, a Content Provider is a well-defined contract to consume and write data into your application. Its traditionally backed by SQLite. But it is just an interface to implement: it can be backed by any data store of your choosing. The implementer must define a unique authority (it can be looked up by your system and uniquely identified).

URI (11:00)

content:// com.example / users / 1
Scheme     Authority     Data    Id

We have four main attributes: the scheme, which defines the protocol (content is common on Android but not necessarily required); the authority is the unique identifier that is assigned to the Content Provider (that the system can look it up); the data it is part of the path, it matches the rejects implemented in the Content Provider itself in a URI measure. This allows us to query the correct data type, select the right table. Lastly, we have an Id for an specific element for the lookup.

Query (11:45)

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(), project, selection, selectionArgs, null, null, sortOrder);
	cursor.setNotificationUri(getContext().getContentResolver(), uri);
	return cursor;
}

We are providing the query method on the Content Provider. We matched the URI, and set the table to the user table. Append an ID from an URI in the switch statement; if it has the ID, append to the where clause. We then apply the remaining methods (passed in by the user: projection, the where, the selection), into the query builder and return a cursor for the consumer to get that data directly into their caller.

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);

Once we built up the Content Provider, we have to be able to access that data. We hit the query method on the content resolver for a URI. The implementation could be used by your own app, or by an external app, depending on the export flag and the permissions. We can also serve files from disk.

Serving files (12:51)

@Override
public ParcelFileDescriptor openFile(Uri uri, String node) throws FileNotFoundException {
	File path = new File(getContext().getCacheDir(), uri.getEncodedPath());
	int imode = o;
	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);
}

The method we are overwriting is the open file on the Content Provider, which returns a ParcelFileDescriptor. There is a similar method, open asset file for sub sections of a file (useful to return a specific resource that exists within your APK). We assume that the URI path handed to us matches our own scheme on disk; if not, you want custom logic to look that up before you went after file paths. The majority of the work is setting the appropriate file mode, based on what is passed in on the method. Then, handling creating the file if it is all right before we pass it on to the helper for opening the file and returning it to the caller.

Getting files (13:45)

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!

The provider serving files would not be useful if we did not have a way to consuming it out. We can use the content resolver to access that method on the Content Provider and return a ParcelFileDescriptor. From that file, we can get a regular Java file descriptor and utilize that in the input or output streams to manipulate the file directly. BitmapFactory has a method to read an image directly out of a file descriptor.

Account Manager (14:14)

The account manager groups the accounts provided by implemented applications on the device. The account can be accessible to other applications for discovery or interaction. It provides helper methods to store data (tokens, account user names) and provide tasks, and supports multiple tokens. It also provides consistency: the user can view and manage their accounts in one location.

Providing Your Own Accounts (15:00)

We need to extend two classes. The first is the AbstractAccountAuthenticator, a service that the account manager communicates with. The second is the AccountAuthenticatorActivity, an activity that you will need to create and extend to handle the login and signup flow, and all of your user flows for your registration.

The process begins by trying to get an account token from the account (see video for a diagram). On success, the callback is called and it will contain a key intent: A) has information on how to start that authenticator activity to take them to the registration or signup flow); or B) it will have the token in the bundle (if you have the bundle, you can use the token directly). Error conditions will also be reported back to the user on the error callback.

AbstractAccountAuthenticator

AbstractAccountAuthenticator - addAccount (16:02)

@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, StringauthTokenType, 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;
}

In our AbstractAccountAuthenticator, on the addAccount is called when a user wants to add an account to your application. If they are in the account manager and they click the add button, this method will be called. You will not want to return a bundle with information on how to start that authentication activity, where the user would signup or register. This is called by the account manager and we proxy that information into a bundle. We send the activity to the account authenticator activity. Then, we set the standard account types and keys that will be communicated back to the account manager. We return that bundle, the account manager starts the intent.

AbstractAccountAuthenticator - getAuthToken (16:46)

@Override
public Bundle getAuthToke(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);
		}
	}
	...
}

The getAuthToken is called by the account manager when requesting a token. First, we extract the user name and password from the account manager (assuming they were both stored there), and ask the server for an appropriate Auth Token.

We try to request a token from the server. Assuming the server hands us back our token, we return it to the calling application - it will bundle directly. But we could not access the user’s password. We need to re-prompt them for their credentials. We need to take then back to the authenticator activity, and they can fill that out by creating an intent to display the authenticator activity.

AccountAuthenticatorActivity - Success (17:43)

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();
}

If we are adding a new account, each application will have its own custom authentication flow. First, we need to create the account based on the user name and account type. If we are adding a new account, we have to add it to the account manager: we cannot return that in the bundle. We set the Auth Token that was returned in the bundle, and we can populate all the extras to be read by the account manager before we return that result. We use the setAccountAuthenticatorResult, read by the account manager. We use the setResult to indicate that it was a successful result and we also put the intent. When we call finish that will return back to the user flow that started the entire process.

Sync Adapter (18:42)

Sync Adapter requires both a Content Provider and the account manager to be implemented before it can be used (often, people create stub versions of the Content Provider and the account manager just to use the sync adapter - Google even has this documented in their official docs).

At a high level, it is efficient for the platform to control syncing your data on a regular interval between your app and a Web Service. It exposes the sync cycle to the user (they can adjust it based on their own usage patterns, battery, data consumption if they are on WiFi or cellular). Since the system optimizes running this when the device may already be waking up for other things, in conjunction with other servers being synced reasonable, it will have a smaller number of wake locks than if you managed it with your own alarm manager. Sync Adapter can be provided and accessed via third party apps. It is great at working with network tickles from GCM pushes.

public class ImagesSyncAdapter extends AbstractThreadSyncAdapter {
	private final AccountManager accountManager;

	public TvShowsSyncAdapter(Context context, boolean autoInitalize) {
		super(context, autoInitalize);
		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);
	}
}

To implement a sync adapter, we subclass the AbstractThreadedSyncAdapter. The most important method is onPerformSync, called to begin the process. This runs in the background thread. In this example, we will use the account manager to get the account that we stored. Then we can request images from our server. Finally, we could store them to disk using the provided Content Provider client.

We also need to create a service for the sync adapter to bind to. This gives access to our resources and account information. After creating the Java classes, we create a metadata class that lives under the res xml folder; it defines how it interacts with the Content Provider and the account manager. Here we specify the authority and the account type and some additional flags (ability for parallel syncs). To wrap that all up, we need to add the service that we bound to in our manifest and be sure to include the metadata file that we referenced in sync adapter.

Sync on a polling schedule (e.g. every hour) is ambitious (instead, analyze your needs of getting data versus the user experience versus battery life): it requests that our sync adapter be kicked off every hour, based on the unique content authority. Most of the network carriers fire down a tickle on the hour (every hour) for keeping network time in sync and getting other information. If you have a large number of users, you get spikes on the hour every hour. A varied approach is to randomly add up to five minutes to the sync interval: we get a more natural distribution over time.

Binding Services for IPC

Binding Services - create AIDL (23:41)

Another way to have inter-process communication on Android is by using bound services with an AIDL (Android Interface Definition Language). We bind that to a standard service, similar to an interface in Java but it uses a .aidl extension. It allows you to define these and it will generate a stub file that you must implement on a service. Bind it outside of your process to communicate directly. This can be used across applications or you can bind to services inside of your own application across processes (but you do not necessarily need the AIDL for inside your own application). Both the sender and the receiver must implement the concrete implementations of the classes, and those classes must be parcelable.

(For an example, see video). We create a service to pass the message between two processes. First, we define the AIDL file with the method. Type support includes the primitives and parcelable classes. Next, we implement the autogenerated stub from the AIDL file. We are going to log the message.

Binding Services - expose interface (24:08)

public class RemoteService extends Service {
	@Override
	public IBinder onBind(Intent intent) {
		return new MessageBinder;
	}
}

Now we need to expose the remote service by returning the binder in the onBind method. Any consumer who attaches to the received, receives the implementation that we designed. The service will cache the results in onBind. If you bind to it multiple times, expect the same results to be returned every time. If you have returned null (e.g. you were not initialized), you can never return the valid state.

Binding Services - connecting (24:37)

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, Content.BIND_AUTO_CREATE);
}
public void onPause() {
	unbindService(connection);
	service = null;
}

We can consume that remote service in another app by setting up a service connection. It receives the callbacks for connected and disconnected states - we know we can call the methods on the service. We need to initiate binding and unbinding of the service connection based on the life cycle of our activity (in the onResume and onPause). onServiceDisconnected is only called when a remote exception happens in the service that you are bound to. You need to null the service yourself in your life cycle termination; otherwise, you can be in an invalid state and have a possible memory leak.

Permissions (25:17)

The first goal of the model is to inform the user. The user is more aware of the risks involved with installing an application. The user will read the dialogue for the permission and make a rational decision. By limiting application access to sensitive APIs, mitigates exploits.

Android permissions fall into four levels:

  • Normal permissions cannot impart harm to the user. For example, change the wallpaper: while apps need to request them, they are granted automatically.
  • Dangerous permissions can impart harm. They could call a number, get the address book from your phone, and apps need to request this specifically with user confirmation.
  • Signature-based permissions are automatically granted by the requesting app if they are signed by the same certificate.
  • SignatureOrSystem level. It uses key store but it allows system-signed apps to access these, designed explicitly for manufacturers.
<permission android:name="com.example.perm.READ"
 android:label="@string/permission_label"
 android:description="@string/permission_description"
 android.protectionLevel="dangerous"
 android:permissionGroup="com.example-group.MYAPP_DATA"
/>

Android gives us the ability to declare our own permissions and secure the objects. They must be provided by the recipient app and declared on the calling app to grant permission. Calling these services that specify requirement without actually requiring the permission would result in a security exception. These permissions can be enforced, programmatically or via flags, set in the Manifest. On Marshmallow for dangerous level permissions and even custom permissions must be requested and granted dynamically at run time by prompting the user.

int canProcess = getContext().checkCallingPermission("com.example.perm.READ");

if (canProcess != PERMISSION_GRANTED) {
	throw new SecurityException("Requires Custom Permission");
}

We call the check calling permission method on a context to determine if the calling application was granted the permission that we need. If it was not, we throw an exception. One caveat to note is that it is easy to leak permissions if a very similar method called checkCallingOrSelfPermission is used instead. That checks the calling application for its permissions, as well as all of the permissions that you provide.

<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" />

If we instead wanted to enforce the permissions via XML, we declare the provider specify both the read and the write permission, our consumer of the Content Provider would just have to declare the user’s permission flag in their Manifest on our custom permission or, if they were on M, then they would have to prompt the users and see they are both listed as dangerous.

Debugging Tips (29:02)

  • Debugging app integrations is challenging. Many integrations are device-specific. Integrations with specific cameras provided by manufacturers is common at bigger companies. Setting debug points in one process and switching between apps becomes troublesome.
  • Logging is crucial.
  • Setting up automated integration tests with the Android tools can help catch early problems. Build mock consumers of your API and run them through the test scenarios.
  • Not all these local APIs on Android are discoverable. Communicate your data contracts through other channels.
  • These integrations can cause frustration when they break down because one app did not handle the setResult on an edit intent. Seek user feedback.
  • Rely on tools for collecting larger amounts of data. Use analytics, use crash reporting to keep an eye on stability and the engagement of your users.

Integration Requirements (30:36)

Returning back to the S Note project:

For getting notes, we implemented both the Intent (single note over to Evernote, easier API) and a Content Provider (sync their entire account over and send a hundred notes at a time) solutions.

For listing notes, we used the intent: takes them into the Evernote UI, shows the list view of all the notes to select.

For creating, composing and updating notes, we did both the intent and the Content Provider (single note cases and larger batches for the notes).

For deleting notes, we did the same (Intent & Content Provider). Intent has an easier API to use; Content Provider is a more efficient API for larger sets of data (you do not necessarily need to pick one versus the other).

For the Account Sync, we used the Account Manager and the Sync Adapter.

Lastly, for getting specific preferences, we used both the Content Provider and a bound service. We used the permissions (permissions are not unique to Samsung). APIs are documented on their website.

Resources (32:25)

  • Dashclock for Evernote: Open source app that I wrote; uses some of the APIs of Evernote: the Content Provider and an AIDL from Dashclock to serve that data over to a widget on your lock screen.
  • Great blogs that cover AIDLs in depth:

I am excited to see more apps in our ecosystem that have tighter integrations. A better, unique, experience, that Android can provide.

Q&A (33:35)

Q:How do you document the environment, the intent and the Content Provider?

Ty: You can generate Java docs, but I would not necessarily recommend that since these are not exactly Java methods to be called. On the Evernote website, they have a developer documentation site where they have a specific integrating with this where they link them all out, and they have examples and the intent data that is required. There is not a spec around. There should be, but it is also minimally used in the ecosystem. Most of the time, you see this only among manufacturers that are having apps talk to each other or larger companies that have suites of apps. Occasionally, you have an app, e.g. Evernote, Dropbox, Google Drive that wants to provide more of a platform. More apps could start using this stuff.

You had an image editing app as another example. Photoshop could do a much better job of handling an edit image intent, returning the result. When I was doing implementations, we wrote a Word doc with a first party implementation and they wanted editing and going back and forth in between all of the attachments that we had. I started playing with that on different other document editors on Android, and no one handles this correctly. It is inconsistent. There should be a spec around it. It’s in Android documentation, but it is easy to be inconsistent with it.


Ty Smith

Ty Smith

Ty has been working on Android since 2009. He is a tech lead at Uber, focusing on the external developer platform. He is a member of the Google Developer Expert program and regularly speaks at international conferences on Android. He organizes the SF Android Meetup group and Droidcon SF. He is a member of the technical advisory and investment group, Specialized Types. Prior to Uber, Ty worked on the Fabric tools at Twitter, the Evernote Android App and SDK, a messaging platform for Sprint, and Zagat for Android.