SpeakEasy.java
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.test.services.speakeasy.server;
import static com.google.common.base.Preconditions.checkNotNull;
import android.app.Activity;
import android.os.IBinder;
import android.util.Log;
import androidx.test.services.speakeasy.SpeakEasyProtocol;
import java.util.HashMap;
import java.util.Map;
/** Contains the core logic of pairing binders with identifiers for SpeakEasy. */
class SpeakEasy {
private static final String TAG = "SpeakEasy";
private final Map<String, Holder> binders = new HashMap<>();
private final BinderDeathCallback binderDeathCallback;
private static final class Holder {
private final IBinder binder;
private final IBinder.DeathRecipient death;
Holder(IBinder binder, IBinder.DeathRecipient death) {
this.binder = binder;
this.death = death;
}
}
SpeakEasy(BinderDeathCallback binderDeathCallback) {
this.binderDeathCallback = checkNotNull(binderDeathCallback);
}
int size() {
return binders.size();
}
void serve(SpeakEasyProtocol sep) {
switch (sep.type) {
case SpeakEasyProtocol.PUBLISH_TYPE:
doPublish(sep.publish);
break;
case SpeakEasyProtocol.REMOVE_TYPE:
doRemove(sep.remove);
break;
case SpeakEasyProtocol.FIND_TYPE:
doFind(sep.find);
break;
default:
throw new IllegalStateException("Invalid/Unknown protocol: " + sep);
}
}
/**
* Looks up a binder for a find request.
*
* <p>Users provide a key to search under, and we will respond back to the ResultReceiver they
* include with their request.
*/
private void doFind(SpeakEasyProtocol.Find f) {
Holder h = binders.get(f.key);
if (null == h) {
f.resultReceiver.send(
Activity.RESULT_OK,
SpeakEasyProtocol.FindResult.asBundle(
false, null, String.format("no binder for key: '%s'", f.key)));
return;
}
f.resultReceiver.send(
Activity.RESULT_OK, SpeakEasyProtocol.FindResult.asBundle(true, h.binder, ""));
}
/** Removes a binder paired with a given key. */
private void doRemove(SpeakEasyProtocol.Remove r) {
Holder h = binders.remove(r.key);
if (h != null) {
if (h.binder.isBinderAlive()) {
h.binder.unlinkToDeath(h.death, 0);
}
}
}
/**
* Pairs a binder with a particular key.
*
* <p>The pairing will succeed if there is no other entry paired with that key and the binder is
* not dead.
*/
private void doPublish(final SpeakEasyProtocol.Publish p) {
if (binders.containsKey(p.key)) {
p.resultReceiver.send(
Activity.RESULT_OK,
SpeakEasyProtocol.PublishResult.asBundle(
p.key, false, String.format("'%s': already in use", p.key)));
return;
}
final String key = p.key;
IBinder.DeathRecipient death =
new IBinder.DeathRecipient() {
@Override
public void binderDied() {
binderDeathCallback.binderDeath(key, p.value);
}
};
try {
p.value.linkToDeath(death, 0);
} catch (Exception e) {
Log.w(TAG, "Super early death of: " + key, e);
binderDeathCallback.binderDeath(key, p.value);
p.resultReceiver.send(
Activity.RESULT_OK,
SpeakEasyProtocol.PublishResult.asBundle(
p.key, false, String.format("'%s': already dead", p.key)));
return;
}
Holder h = new Holder(p.value, death);
binders.put(p.key, h);
p.resultReceiver.send(
Activity.RESULT_OK, SpeakEasyProtocol.PublishResult.asBundle(p.key, true, "published"));
}
/** Notifies listeners when a binder has died. */
interface BinderDeathCallback {
public void binderDeath(String key, IBinder dead);
}
}