Skip to content

Instantly share code, notes, and snippets.

@oasisfeng
Created May 26, 2016 13:46
Show Gist options
  • Save oasisfeng/71d09369fddaa742fabf2e4d3832abcf to your computer and use it in GitHub Desktop.
Save oasisfeng/71d09369fddaa742fabf2e4d3832abcf to your computer and use it in GitHub Desktop.
Service framework for friendly and efficient AIDL service binding in Android app.
package com.oasisfeng.android.service;
import android.annotation.SuppressLint;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.IInterface;
import android.support.annotation.Nullable;
import android.util.Log;
import com.oasisfeng.nevo.shared.BuildConfig;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
/**
z * A base class for AIDL service to simplify the boilerplate code, supporting both local and remote clients simultaneously.
*
* <p>Remember to declare your service in AndroidManifest.xml as:
* <b>(be sure to set android:exported="false" if you have no intention to expose this service)</b>
* <pre>
* &lt;service android:name=".DemoService" android:exported="false"&gt;
* &lt;intent-filter&gt;
* &lt;action android:name="com.taobao.android.service.IDemoService" /&gt;
* &lt;/intent-filter&gt;
* &lt;/service&gt;
* </pre>
*
* <pre>
* public class DemoService extends AidlService&lt;IDemoService.Stub&gt; {
*
* public class Impl extends IDemoService.Stub implements Closeable {
*
* protected String mName;
*
* &#64;Override public String helloWorld(final String name) {
* mName = name;
* return "Hello " + name;
* }
*
* public void close() {
* // Optional destruction code here, as of onDestroy() in service.
* // No need to implement Closeable (or AutoCloseable) if nothing to do in destruction.
* }
* }
*
* protected IDemoService.Stub createBinder() {
* return new Impl();
* }
* }
* </pre>
*
* @author Oasis
*/
public abstract class AidlService<Stub extends Binder & IInterface> extends Service {
public AidlService() {
super.onCreate();
final Type[] types = getActualTypeArguments(getClass());
final Type type = types[0];
if (! (type instanceof Class) || ! Binder.class.isAssignableFrom((Class) type))
throw new IllegalArgumentException(type + " is not an AIDL stub");
//noinspection unchecked
mInterface = (Class<? extends IInterface>) ((Class) type).getInterfaces()[0];
if (BuildConfig.DEBUG)
for (Class<?> clazz = getClass(); clazz != AidlService.class; clazz = clazz.getSuperclass())
for (final Field field : clazz.getDeclaredFields())
if (! Modifier.isStatic(field.getModifiers()))
throw new IllegalStateException("AidlService-derived class must be stateless. " +
"Unlike Android service, it is not singleton. " +
"Consider moving fields to the binder class.");
}
/**
* Put initialization code in {@link #createBinder()} instead of this method.
* Only the binder, not the service instance is singleton.
*/
@Override public final void onCreate() { super.onCreate(); }
@Override public final IBinder onBind(final Intent intent) {
Log.d(toString(), "onBind");
mBinder = LocalAidlServices.bind(this, mInterface, intent);
return mBinder != null ? mBinder.asBinder() : null;
}
@Nullable Stub onBindFromLocal() {
final Stub stub = createBinder();
mBinder = stub;
return stub;
}
/**
* Create the binder instance. This method will be called only once
* until the returned stub is no longer used (and thus closed).
*/
protected abstract @Nullable Stub createBinder();
void closeBinder() {
if (mBinder == null) throw new IllegalStateException("binder is null");
synchronized (this) {
if (mBinder instanceof AutoCloseable) close((AutoCloseable) mBinder);
mBinder = null;
}
}
@SuppressLint("NewApi") // AutoCloseable is hidden but accessible
private void close(final AutoCloseable closeable) {
try {
Log.d(toString(), "close");
closeable.close();
} catch (final Exception e) {
Log.w(toString(), "Error closing " + closeable, e);
}
}
/** Called by AMS after all remote clients disconnect, while local bindings could be still in use. */
@Override public final boolean onUnbind(final Intent intent) {
if (mBinder != null) LocalAidlServices.unbind(this, mBinder);
return false;
}
/**
* Put destruction code in {@link AutoCloseable#close()} of the stub instead of this method.
* Only the binder, not the service instance is singleton.
*/
@Override public final void onDestroy() { super.onDestroy(); }
private static Type[] getActualTypeArguments(Class<?> derivation) {
while (derivation != null) {
final Type type = derivation.getGenericSuperclass();
if (type instanceof ParameterizedType) {
final ParameterizedType ptype = (ParameterizedType) type;
if (AidlService.class.equals(ptype.getRawType()))
return ptype.getActualTypeArguments();
}
derivation = derivation.getSuperclass();
}
throw new IllegalArgumentException();
}
@Override public final int onStartCommand(final Intent intent, final int flags, final int startId) {
Log.w(toString(), "Start operation is not allowed for AIDL service.");
stopSelf(startId);
return START_NOT_STICKY;
}
@Override public String toString() {
if (mName != null) return mName;
final String name = getSimpleName();
return mName = name.substring(name.indexOf('$') + 1);
}
private String getSimpleName() {
final String name = getClass().getName();
if (name.endsWith("$Service")) return name.substring(name.lastIndexOf('.') + 1, name.length() - 8/* "$Service".length */);
return name.substring(name.lastIndexOf('.') + 1);
}
private final Class<? extends IInterface> mInterface;
private String mName;
private IInterface mBinder;
}
package com.oasisfeng.android.service;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Application;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentCallbacks;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Debug;
import android.os.IBinder;
import android.os.IInterface;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.oasisfeng.android.util.MultiCatchROECompat;
import java.io.Closeable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* Optimized implementation for local-only service.
*
* No more AMS IPC involved, no more asynchronous pains.
*
* @author Oasis
*/
class LocalAidlServices {
/**
* Two incoming paths:
* Local-binding: bindWith() -> AidlService.onCreate(), onBind() -> bindWith()
* Sys-binding: ActivityThread.handleCreateService() -> AidlService.onCreate(), onBind() -> bindWith()
*
* @param intent must be explicit (with component pre-resolved)
*/
static @Nullable <I extends IInterface> I bind(final Context context, @NonNull final Class<I> service_interface, final Intent intent) {
if (intent.getComponent() == null)
throw new IllegalArgumentException("Intent must be explicit (with component set)");
//noinspection SynchronizationOnLocalVariableOrMethodParameter,
synchronized (service_interface) { // Use interface class as a sparse lock to avoid locking sServices during service initialization.
return bindLocked(context, service_interface, intent);
}
}
private static @Nullable <I extends IInterface> I bindLocked(final Context context, @NonNull final Class<I> service_interface, final Intent intent) {
final IBinder binder;
ServiceRecord record = sServices.get(service_interface);
if (record == null) {
final IBinder sys_binder = sDummyReceiver.peekService(context, intent);
if (sys_binder != null) { // The service is already bound by system, initiate a new binding via AMS.
record = new ServiceRecord(service_interface, null, sys_binder);
if (! context.bindService(intent, record, Context.BIND_AUTO_CREATE)) {
Log.e(TAG, "Failed to bind service with " + intent);
return null;
}
} else { // Create the service instance locally
final Service service = createService(context, intent.getComponent().getClassName());
if (service == null) return null;
binder = bindService(service, intent);
if (binder == null) {
destroyService(service);
return null;
}
record = new ServiceRecord(service_interface, service, binder);
}
sServices.put(service_interface, record);
}
// Wrap with dynamic proxy, which is later used to differentiate instances in unbind().
final ProxyHandler handler = new ProxyHandler(service_interface, record.binder);
final Class[] interfaces = collectInterfacesToProxy(record.binder, service_interface);
@SuppressWarnings("unchecked") final I instance = (I) Proxy.newProxyInstance(context.getClassLoader(), interfaces, handler);
record.instances.add(instance);
return instance;
}
private static Class[] collectInterfacesToProxy(final Object impl, final Class<?> service_interface) {
final Class<?>[] direct_interfaces = impl.getClass().getInterfaces();
if (direct_interfaces.length == 0) return new Class[] { service_interface };
for (int i = 0; i < direct_interfaces.length; i ++) {
final Class<?> direct_interface = direct_interfaces[i];
if (direct_interface == Closeable.class || direct_interface == AutoCloseable.class) { // Exclude
direct_interfaces[i] = service_interface;
return direct_interfaces;
}
}
final Class<?>[] merged = Arrays.copyOf(direct_interfaces, direct_interfaces.length + 1);
merged[direct_interfaces.length] = service_interface;
return merged;
}
static <I extends IInterface> boolean unbind(final Context context, final I instance) {
if (instance == null) throw new IllegalArgumentException("instance is null");
final InvocationHandler invocation_handler;
final Class<? extends IInterface> proxy_class = instance.getClass();
if (! Proxy.isProxyClass(proxy_class)
|| ! ((invocation_handler = Proxy.getInvocationHandler(instance)) instanceof ProxyHandler))
throw new IllegalArgumentException("Not a service instance: " + instance);
final ProxyHandler handler = (ProxyHandler) invocation_handler;
handler.binder = null; // Invalidate the proxy
return unbind(context, handler.itf, instance);
}
private static <I extends IInterface> boolean unbind(final Context context, final Class<? extends IInterface> service_interface, final I instance) {
final ServiceRecord record = sServices.get(service_interface);
if (record == null) throw new IllegalArgumentException("No service bound for " + service_interface.getName());
final Iterator iterator = record.instances.iterator();
// List.remove() is not working on list of dynamic proxies, since equals() method is forwarded.
while (iterator.hasNext()) if (iterator.next() == instance) {
iterator.remove();
if (record.instances.isEmpty()) {
sServices.remove(service_interface);
// TODO: Defer the unbinding and destroying
if (record.service == null)
try {
context.unbindService(record);
} catch (final RuntimeException e) {
Log.d(TAG, "Ignore failure in service unbinding: " + e);
}
else {
unbindService(record.service, makeIntent(record.service, service_interface));
destroyService(record.service);
}
return true;
} else return false;
}
throw new IllegalArgumentException("Instance not found in service connections of " + service_interface + ": "
+ instance.getClass().getName() + "@" + System.identityHashCode(instance));
}
private static class ProxyHandler implements InvocationHandler {
private ProxyHandler(final Class<? extends IInterface> service_interface, final IBinder binder) {
this.binder = binder;
this.itf = service_interface;
}
@Override public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
if (binder == null) throw new IllegalStateException("Service is already unbound");
try {
return method.invoke(binder, args);
} catch (final InvocationTargetException e) {
throw e.getTargetException();
}
}
private IBinder binder;
private final Class<? extends IInterface> itf;
}
private static Intent makeIntent(final Context context, final Class<? extends IInterface> service_interface) {
return new Intent(service_interface.getName()).setPackage(context.getPackageName());
}
private static void unbindService(final Service service, final Intent intent) {
if (service instanceof AidlService)
((AidlService) service).closeBinder();
else try {
final boolean rebind = service.onUnbind(intent); // TODO: Support rebind if true is returned.
if (rebind) throw new UnsupportedOperationException("Sorry, onRebind() is not yet supported.");
} catch (final RuntimeException e) {
Log.e(TAG, "Error unbinding " + service, e);
}
}
static void destroyService(final Service service) {
unregisterComponentCallbacks(service.getApplication(), service);
try {
final long start = Debug.threadCpuTimeNanos();
service.onDestroy();
logExcessiveElapse(start, 5, service, ".onDestroy()");
} catch (final RuntimeException e) {
Log.e(TAG, "Error destroying " + service, e);
}
}
static @Nullable Service createService(final Context context, final String classname) {
// Load class
final Class<?> clazz;
try {
clazz = context.getClassLoader().loadClass(classname);
} catch (final ClassNotFoundException e) {
Log.w(TAG, e.toString());
return null;
}
if (! Service.class.isAssignableFrom(clazz)) {
Log.w(TAG, "Not service class: " + clazz);
return null;
}
@SuppressWarnings("unchecked") final Class<? extends Service> service_class = (Class<? extends Service>) clazz;
// Instantiate
final Service service; final String class_name;
try {
final long start = Debug.threadCpuTimeNanos();
service = service_class.newInstance();
logExcessiveElapse(start, 2, class_name = service_class.getName(), "()");
} catch (final InstantiationException e) {
Log.e(TAG, "Failed to instantiate " + classname, e);
return null;
} catch (final IllegalAccessException e) {
Log.e(TAG, "Constructor of " + classname + " is inaccessible", e);
return null;
} catch (final RuntimeException e) {
Log.e(TAG, "Error instantiating " + classname, e);
return null;
}
// Attach
final Application application = getApplication(context);
attach(context, service_class, service, application);
// Create
try {
final long start = Debug.threadCpuTimeNanos();
service.onCreate();
logExcessiveElapse(start, 5, class_name, ".onCreate()");
} catch (final RuntimeException e) {
Log.e(TAG, service + ".onCreate()", e);
}
// Hookup lifecycle callbacks
registerComponentCallbacks(service.getApplication(), service);
return service;
}
private static @Nullable IBinder bindService(final Service service, final Intent intent) {
// Bind
IBinder binder = null;
try {
final long start = Debug.threadCpuTimeNanos();
binder = service instanceof AidlService ? ((AidlService) service).onBindFromLocal() : service.onBind(intent);
logExcessiveElapse(start, 2, service, ".onBind()");
} catch (final RuntimeException e) {
Log.e(TAG, service + ".onBind()", e);
}
if (binder == null) { // Error running onBind() or null is returned by onBind().
destroyService(service);
try {
final long start = Debug.threadCpuTimeNanos();
service.onDestroy();
logExcessiveElapse(start, 5, service, ".onDestroy()");
} catch (final RuntimeException e) {
Log.e(TAG, service + ".onDestroy()", e);
}
}
return binder;
}
private static void logExcessiveElapse(final long start_thread_cpu_nanos, final long tolerable_duration_ms, final Object procedure, final String postfix) {
final long duration_ms = (Debug.threadCpuTimeNanos() - start_thread_cpu_nanos) / 1_000_000;
if (duration_ms <= tolerable_duration_ms) return;
Log.w(TAG, procedure.toString() + (postfix != null ? postfix : "") + " consumed " + duration_ms + "ms (thread CPU time)");
}
@TargetApi(VERSION_CODES.ICE_CREAM_SANDWICH)
private static void registerComponentCallbacks(final Application app, final ComponentCallbacks callbacks) {
if (VERSION.SDK_INT < VERSION_CODES.ICE_CREAM_SANDWICH) return;
app.registerComponentCallbacks(callbacks);
}
@TargetApi(VERSION_CODES.ICE_CREAM_SANDWICH)
private static void unregisterComponentCallbacks(final Application app, final ComponentCallbacks callbacks) {
if (app == null || VERSION.SDK_INT < VERSION_CODES.ICE_CREAM_SANDWICH) return;
app.unregisterComponentCallbacks(callbacks);
}
private static void attach(final Context context, final Class<? extends Service> service_class,
final Service service, final Application application) {
if (Service_attach == null) return;
try {
Service_attach.invoke(service, context, null, service_class.getName(), null, application, null);
} catch (final IllegalAccessException e) {
Log.e(TAG, "Unexpected exception when attaching service.", e);
} catch (final InvocationTargetException e) {
throw new RuntimeException(e.getTargetException());
}
}
private static Application getApplication(final Context context) {
if (context instanceof Activity) return ((Activity) context).getApplication();
if (context instanceof Service) return ((Service) context).getApplication();
final Context app_context = context.getApplicationContext();
if (app_context instanceof Application) return (Application) app_context;
Log.w(TAG, "Cannot discover application from context " + context);
return null;
}
private static final Map<Class<? extends IInterface>, ServiceRecord> sServices = Collections.synchronizedMap(new HashMap<>());
private static final BroadcastReceiver sDummyReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) {}};
private static final String TAG = "LocalSvc";
// Method signature: (useless parameters for AIDL service - thread, token, activityManager)
// public final void attach(Context context, ActivityThread thread, String className, IBinder token, Application application, Object activityManager)
private static final Method Service_attach;
static {
Method method = null;
try {
final Class<?> ActivityThread = Class.forName("android.app.ActivityThread");
method = Service.class.getDeclaredMethod("attach", Context.class, ActivityThread, String.class,
IBinder.class, Application.class, Object.class);
method.setAccessible(true);
} catch (final ClassNotFoundException | NoSuchMethodException | MultiCatchROECompat e) {
Log.e(TAG, "Incompatible ROM", e);
}
Service_attach = method;
}
private static class ServiceRecord implements ServiceConnection {
final Class<? extends IInterface> itf;
final @Nullable Service service;
final IBinder binder;
final List<IInterface> instances = new ArrayList<>();
@Override public void onServiceConnected(final ComponentName name, final IBinder binder) {
if (binder != this.binder) Log.e(TAG, "Inconsistent binder: " + binder + " != " + this.binder);
}
@Override public void onServiceDisconnected(final ComponentName name) {
// TODO
}
ServiceRecord(final Class<? extends IInterface> service_interface, final @Nullable Service service, final IBinder binder) {
itf = service_interface;
this.service = service;
this.binder = binder;
}
}
}
package com.oasisfeng.android.service;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Process;
import android.support.annotation.CheckResult;
import android.support.annotation.Nullable;
import android.util.Log;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
/**
* Simplify the AIDL service usage
*
* Created by Oasis on 2015/5/19.
*/
public class Services {
public interface ServiceReadyThrows<I extends IInterface, E extends Exception> {
void onServiceReady(I service) throws E;
}
public interface AidlStub<I extends IInterface> {
I asInterface(IBinder obj);
}
/** Bind to the specified service for one-time procedure, then unbind the service. */
public static <I extends IInterface, E extends Exception> boolean use(final Context context, final Class<I> itf, final AidlStub<I> stub, final ServiceReadyThrows<I, E> procedure) {
return bind(context, itf, new ServiceConnection() {
@Override public void onServiceConnected(final ComponentName name, final IBinder binder) {
final I service = stub.asInterface(binder);
try {
procedure.onServiceReady(service);
} catch (final Exception e) {
Log.e(TAG, "Error in " + procedure, e);
} finally {
try { context.unbindService(this); } catch (final RuntimeException ignored) {}
}
}
@Override public void onServiceDisconnected(final ComponentName name) {}
});
}
public static boolean bind(final Context context, final Class<? extends IInterface> service_interface, final ServiceConnection conn) {
final Intent service_intent = buildServiceIntent(context, service_interface);
if (service_intent == null) {
Log.w(TAG, "No matched service for " + service_interface.getName());
return false;
}
return context.bindService(service_intent, conn, Context.BIND_AUTO_CREATE);
}
public static @CheckResult @Nullable <I extends IInterface> I bindLocal(final Context context, final Class<I> service_interface) {
if (! sRegistry.isEmpty()) {
@SuppressWarnings("unchecked") final I instance = (I) sRegistry.get(service_interface);
if (instance != null) return instance;
}
return LocalAidlServices.bind(context, service_interface, buildServiceIntent(context, service_interface));
}
public static @Nullable Service createLocal(final Context context, final String classname) {
return LocalAidlServices.createService(context, classname);
}
public static void destroyLocal(final Service service) {
LocalAidlServices.destroyService(service);
}
public static <I extends IInterface> void unbindLocal(final Context context, final I instance) {
if (sRegistry.containsValue(instance)) return; // TODO: Reference counting registered services
LocalAidlServices.unbind(context, instance);
}
public static <I extends IInterface> void register(final Class<I> service_interface, final I instance) {
final IInterface existent = sRegistry.get(service_interface);
if (existent != null) {
if (existent == instance) return;
throw new IllegalStateException(service_interface + " was already registered with " + existent.getClass());
}
sRegistry.put(service_interface, instance);
}
public static <I extends IInterface> boolean unregister(final Class<I> service_interface, final I instance) {
final IInterface existent = sRegistry.get(service_interface);
if (existent == null) return false;
if (instance != existent) throw new IllegalStateException(service_interface + " was registered with " + existent + ", but being unregistered with " + instance);
sRegistry.remove(service_interface);
return true;
}
public static <I extends IInterface> I peekService(final Context context, final Class<I> service_interface) {
final IBinder binder = peekService(context, buildServiceIntent(context, service_interface));
if (binder == null) return null;
try { //noinspection unchecked
return (I) service_interface.getMethod("asInterface", IBinder.class).invoke(binder);
} catch (final Exception e) {
Log.e(TAG, "Error calling " + service_interface.getCanonicalName() + ".asInterface() on " + binder, e);
return null;
}
}
public static IBinder peekService(final Context context, final Intent intent) {
return sDummyReceiver.peekService(context, intent);
}
private static @Nullable Intent buildServiceIntent(final Context context, final Class<?> service_interface) {
final String name = service_interface.getName(); final Intent intent = new Intent(name);
final PackageManager pm = context.getPackageManager(); final int uid = android.os.Process.myUid();
final ComponentName component = resolveServiceIntent(context, intent, (left, right) -> {
final ApplicationInfo app_left = left.serviceInfo.applicationInfo, app_right = right.serviceInfo.applicationInfo;
// 1 - UID
int rank_left = app_left.uid != uid ? 1 : 0;
int rank_right = app_right.uid != uid ? 1 : 0;
if (rank_left != rank_right) return rank_left - rank_right;
// 2 - Priority
final int priority_diff = left.priority - right.priority;
if (priority_diff != 0) return (- priority_diff); // Higher priority wins
// 3 - Package update time
if (! app_left.packageName.equals(app_right.packageName)) {
final PackageInfo pkg_left, pkg_right;
try {
pkg_left = pm.getPackageInfo(app_left.packageName, 0);
} catch (final PackageManager.NameNotFoundException e) { return 1; } // right wins
try {
pkg_right = pm.getPackageInfo(app_right.packageName, 0);
} catch (final PackageManager.NameNotFoundException e) { return - 1; } // left wins
if (pkg_left.lastUpdateTime != pkg_right.lastUpdateTime)
return (int) - (pkg_left.lastUpdateTime - pkg_right.lastUpdateTime); // Newer wins
}
// 4 - Package name
final String pkg = context.getPackageName();
rank_left = pkg.equals(app_left.packageName) ? 0 : 1;
rank_right = pkg.equals(app_right.packageName) ? 0 : 1;
if (rank_left != rank_right) return rank_left - rank_right;
return 0;
});
if (component == null) return null;
intent.setComponent(component);
return intent;
}
/** @param comparator comparator for resolving candidates, smaller wins */
private static @Nullable ComponentName resolveServiceIntent(final Context context, final Intent intent, final Comparator<ResolveInfo> comparator) {
final List<ResolveInfo> matches = context.getPackageManager().queryIntentServices(intent, 0);
if (matches == null || matches.isEmpty()) return null;
ResolveInfo best_match = matches.get(0);
final int my_uid = Process.myUid();
for (final ResolveInfo match : matches) {
if (! match.serviceInfo.exported && match.serviceInfo.applicationInfo.uid != my_uid) continue;
if (best_match != match && comparator.compare(match, best_match) < 0)
best_match = match;
}
final ComponentName component = new ComponentName(best_match.serviceInfo.packageName, best_match.serviceInfo.name);
if (matches.size() > 1) Log.d(TAG, "Final match for " + intent + " among " + matches.size() + ": " + component.flattenToShortString());
return component;
}
private static final Map<Class, IInterface> sRegistry = new IdentityHashMap<>();
private static final BroadcastReceiver sDummyReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) {}};
private static final String TAG = "Services";
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment