GCM: Add support for message acking, deliver to all receivers of package when working permissionless

This commit is contained in:
Marvin W 2020-08-22 23:43:14 +02:00
parent 74c0e28e27
commit 60cc63ed60
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
4 changed files with 168 additions and 73 deletions

View File

@ -30,6 +30,7 @@ public final class GcmConstants {
public static final String ACTION_INSTANCE_ID = "com.google.android.gms.iid.InstanceID"; public static final String ACTION_INSTANCE_ID = "com.google.android.gms.iid.InstanceID";
public static final String EXTRA_APP = "app"; public static final String EXTRA_APP = "app";
public static final String EXTRA_APP_OVERRIDE = "org.microg.gms.gcm.APP_OVERRIDE";
public static final String EXTRA_APP_ID = "appid"; public static final String EXTRA_APP_ID = "appid";
public static final String EXTRA_APP_VERSION_CODE = "app_ver"; public static final String EXTRA_APP_VERSION_CODE = "app_ver";
public static final String EXTRA_APP_VERSION_NAME = "app_ver_name"; public static final String EXTRA_APP_VERSION_NAME = "app_ver_name";

View File

@ -36,10 +36,12 @@ public final class McsConstants {
public static final int MSG_TEARDOWN = 30; public static final int MSG_TEARDOWN = 30;
public static final int MSG_CONNECT = 40; public static final int MSG_CONNECT = 40;
public static final int MSG_HEARTBEAT = 41; public static final int MSG_HEARTBEAT = 41;
public static final int MSG_ACK = 42;
public static String ACTION_CONNECT = "org.microg.gms.gcm.mcs.CONNECT"; public static String ACTION_CONNECT = "org.microg.gms.gcm.mcs.CONNECT";
public static String ACTION_RECONNECT = "org.microg.gms.gcm.mcs.RECONNECT"; public static String ACTION_RECONNECT = "org.microg.gms.gcm.mcs.RECONNECT";
public static String ACTION_HEARTBEAT = "org.microg.gms.gcm.mcs.HEARTBEAT"; public static String ACTION_HEARTBEAT = "org.microg.gms.gcm.mcs.HEARTBEAT";
public static String ACTION_SEND = "org.microg.gms.gcm.mcs.SEND"; public static String ACTION_SEND = "org.microg.gms.gcm.mcs.SEND";
public static String ACTION_ACK = "org.microg.gms.gcm.mcs.ACK";
public static String EXTRA_REASON = "org.microg.gms.gcm.mcs.REASON"; public static String EXTRA_REASON = "org.microg.gms.gcm.mcs.REASON";
} }

View File

@ -26,6 +26,7 @@ import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.PermissionInfo;
import android.content.pm.ResolveInfo; import android.content.pm.ResolveInfo;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.os.Build; import android.os.Build;
@ -51,8 +52,10 @@ import org.microg.gms.common.PackageUtils;
import org.microg.gms.gcm.mcs.AppData; import org.microg.gms.gcm.mcs.AppData;
import org.microg.gms.gcm.mcs.Close; import org.microg.gms.gcm.mcs.Close;
import org.microg.gms.gcm.mcs.DataMessageStanza; import org.microg.gms.gcm.mcs.DataMessageStanza;
import org.microg.gms.gcm.mcs.Extension;
import org.microg.gms.gcm.mcs.HeartbeatAck; import org.microg.gms.gcm.mcs.HeartbeatAck;
import org.microg.gms.gcm.mcs.HeartbeatPing; import org.microg.gms.gcm.mcs.HeartbeatPing;
import org.microg.gms.gcm.mcs.IqStanza;
import org.microg.gms.gcm.mcs.LoginRequest; import org.microg.gms.gcm.mcs.LoginRequest;
import org.microg.gms.gcm.mcs.LoginResponse; import org.microg.gms.gcm.mcs.LoginResponse;
import org.microg.gms.gcm.mcs.Setting; import org.microg.gms.gcm.mcs.Setting;
@ -64,6 +67,7 @@ import java.net.Socket;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
@ -74,6 +78,7 @@ import static android.os.Build.VERSION.SDK_INT;
import static org.microg.gms.common.ForegroundServiceContext.EXTRA_FOREGROUND; import static org.microg.gms.common.ForegroundServiceContext.EXTRA_FOREGROUND;
import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_RECEIVE; import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_RECEIVE;
import static org.microg.gms.gcm.GcmConstants.EXTRA_APP; import static org.microg.gms.gcm.GcmConstants.EXTRA_APP;
import static org.microg.gms.gcm.GcmConstants.EXTRA_APP_OVERRIDE;
import static org.microg.gms.gcm.GcmConstants.EXTRA_COLLAPSE_KEY; import static org.microg.gms.gcm.GcmConstants.EXTRA_COLLAPSE_KEY;
import static org.microg.gms.gcm.GcmConstants.EXTRA_FROM; import static org.microg.gms.gcm.GcmConstants.EXTRA_FROM;
import static org.microg.gms.gcm.GcmConstants.EXTRA_MESSAGE_ID; import static org.microg.gms.gcm.GcmConstants.EXTRA_MESSAGE_ID;
@ -82,6 +87,7 @@ import static org.microg.gms.gcm.GcmConstants.EXTRA_REGISTRATION_ID;
import static org.microg.gms.gcm.GcmConstants.EXTRA_SEND_FROM; import static org.microg.gms.gcm.GcmConstants.EXTRA_SEND_FROM;
import static org.microg.gms.gcm.GcmConstants.EXTRA_SEND_TO; import static org.microg.gms.gcm.GcmConstants.EXTRA_SEND_TO;
import static org.microg.gms.gcm.GcmConstants.EXTRA_TTL; import static org.microg.gms.gcm.GcmConstants.EXTRA_TTL;
import static org.microg.gms.gcm.McsConstants.ACTION_ACK;
import static org.microg.gms.gcm.McsConstants.ACTION_CONNECT; import static org.microg.gms.gcm.McsConstants.ACTION_CONNECT;
import static org.microg.gms.gcm.McsConstants.ACTION_HEARTBEAT; import static org.microg.gms.gcm.McsConstants.ACTION_HEARTBEAT;
import static org.microg.gms.gcm.McsConstants.ACTION_RECONNECT; import static org.microg.gms.gcm.McsConstants.ACTION_RECONNECT;
@ -91,8 +97,10 @@ import static org.microg.gms.gcm.McsConstants.MCS_CLOSE_TAG;
import static org.microg.gms.gcm.McsConstants.MCS_DATA_MESSAGE_STANZA_TAG; import static org.microg.gms.gcm.McsConstants.MCS_DATA_MESSAGE_STANZA_TAG;
import static org.microg.gms.gcm.McsConstants.MCS_HEARTBEAT_ACK_TAG; import static org.microg.gms.gcm.McsConstants.MCS_HEARTBEAT_ACK_TAG;
import static org.microg.gms.gcm.McsConstants.MCS_HEARTBEAT_PING_TAG; import static org.microg.gms.gcm.McsConstants.MCS_HEARTBEAT_PING_TAG;
import static org.microg.gms.gcm.McsConstants.MCS_IQ_STANZA_TAG;
import static org.microg.gms.gcm.McsConstants.MCS_LOGIN_REQUEST_TAG; import static org.microg.gms.gcm.McsConstants.MCS_LOGIN_REQUEST_TAG;
import static org.microg.gms.gcm.McsConstants.MCS_LOGIN_RESPONSE_TAG; import static org.microg.gms.gcm.McsConstants.MCS_LOGIN_RESPONSE_TAG;
import static org.microg.gms.gcm.McsConstants.MSG_ACK;
import static org.microg.gms.gcm.McsConstants.MSG_CONNECT; import static org.microg.gms.gcm.McsConstants.MSG_CONNECT;
import static org.microg.gms.gcm.McsConstants.MSG_HEARTBEAT; import static org.microg.gms.gcm.McsConstants.MSG_HEARTBEAT;
import static org.microg.gms.gcm.McsConstants.MSG_INPUT; import static org.microg.gms.gcm.McsConstants.MSG_INPUT;
@ -119,6 +127,7 @@ public class McsService extends Service implements Handler.Callback {
private static long lastIncomingNetworkRealtime = 0; private static long lastIncomingNetworkRealtime = 0;
private static long startTimestamp = 0; private static long startTimestamp = 0;
public static String activeNetworkPref = null; public static String activeNetworkPref = null;
private AtomicInteger nextMessageId = new AtomicInteger(0x1000000);
private static Socket sslSocket; private static Socket sslSocket;
private static McsInputStream inputStream; private static McsInputStream inputStream;
@ -307,6 +316,8 @@ public class McsService extends Service implements Handler.Callback {
rootHandler.sendMessage(rootHandler.obtainMessage(MSG_HEARTBEAT, reason)); rootHandler.sendMessage(rootHandler.obtainMessage(MSG_HEARTBEAT, reason));
} else if (ACTION_SEND.equals(intent.getAction())) { } else if (ACTION_SEND.equals(intent.getAction())) {
handleSendMessage(intent); handleSendMessage(intent);
} else if (ACTION_ACK.equals(intent.getAction())) {
rootHandler.sendMessage(rootHandler.obtainMessage(MSG_ACK, reason));
} }
WakefulBroadcastReceiver.completeWakefulIntent(intent); WakefulBroadcastReceiver.completeWakefulIntent(intent);
} else if (connectIntent == null) { } else if (connectIntent == null) {
@ -347,12 +358,20 @@ public class McsService extends Service implements Handler.Callback {
Log.w(TAG, "Failed to send message, missing package name"); Log.w(TAG, "Failed to send message, missing package name");
return; return;
} }
if (packageName.equals(getPackageName()) && intent.hasExtra(EXTRA_APP_OVERRIDE)) {
packageName = intent.getStringExtra(EXTRA_APP_OVERRIDE);
intent.removeExtra(EXTRA_APP_OVERRIDE);
}
intent.removeExtra(EXTRA_APP); intent.removeExtra(EXTRA_APP);
int ttl; int ttl;
try { try {
ttl = Integer.parseInt(intent.getStringExtra(EXTRA_TTL)); if (intent.hasExtra(EXTRA_TTL)) {
if (ttl < 0 || ttl > maxTtl) { ttl = Integer.parseInt(intent.getStringExtra(EXTRA_TTL));
if (ttl < 0 || ttl > maxTtl) {
ttl = maxTtl;
}
} else {
ttl = maxTtl; ttl = maxTtl;
} }
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
@ -403,7 +422,8 @@ public class McsService extends Service implements Handler.Callback {
try { try {
DataMessageStanza msg = new DataMessageStanza.Builder() DataMessageStanza msg = new DataMessageStanza.Builder()
.sent(System.currentTimeMillis() / 1000L) .sent(System.currentTimeMillis() / 1000L)
.id(messageId) .id(Integer.toHexString(nextMessageId.incrementAndGet()))
.persistent_id(messageId)
.token(collapseKey) .token(collapseKey)
.from(from) .from(from)
.reg_id(registrationId) .reg_id(registrationId)
@ -513,9 +533,11 @@ public class McsService extends Service implements Handler.Callback {
Intent intent = new Intent(); Intent intent = new Intent();
intent.setAction(ACTION_C2DM_RECEIVE); intent.setAction(ACTION_C2DM_RECEIVE);
intent.setPackage(packageName);
intent.putExtra(EXTRA_FROM, msg.from); intent.putExtra(EXTRA_FROM, msg.from);
intent.putExtra(EXTRA_MESSAGE_ID, msg.id); intent.putExtra(EXTRA_MESSAGE_ID, msg.id);
if (msg.persistent_id != null) {
intent.putExtra(EXTRA_MESSAGE_ID, msg.persistent_id);
}
if (app.wakeForDelivery) { if (app.wakeForDelivery) {
intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
} else { } else {
@ -526,41 +548,52 @@ public class McsService extends Service implements Handler.Callback {
intent.putExtra(appData.key, appData.value); intent.putExtra(appData.key, appData.value);
} }
String receiverPermission; String receiverPermission = null;
try { try {
String name = packageName + ".permission.C2D_MESSAGE"; String name = packageName + ".permission.C2D_MESSAGE";
getPackageManager().getPermissionInfo(name, 0); PermissionInfo info = getPackageManager().getPermissionInfo(name, 0);
receiverPermission = name; if (info.packageName.equals(packageName)) {
} catch (PackageManager.NameNotFoundException e) { receiverPermission = name;
receiverPermission = null; }
} catch (Exception ignored) {
// Keep null, no valid permission found
} }
List<ResolveInfo> infos = getPackageManager().queryBroadcastReceivers(intent, PackageManager.GET_RESOLVED_FILTER); if (receiverPermission == null) {
if (infos == null || infos.isEmpty()) { // Without receiver permission, we only restrict by package name
logd("No target for message, wut?"); logd("Deliver message to all receivers in package " + packageName);
intent.setPackage(packageName);
sendOrderedBroadcast(intent, null);
} else { } else {
for (ResolveInfo resolveInfo : infos) { List<ResolveInfo> infos = getPackageManager().queryBroadcastReceivers(intent, PackageManager.GET_RESOLVED_FILTER);
logd("Target: " + resolveInfo); if (infos == null || infos.isEmpty()) {
Intent targetIntent = new Intent(intent); logd("No target for message, wut?");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && app.wakeForDelivery) { } else {
try { for (ResolveInfo resolveInfo : infos) {
if (getUserIdMethod != null && addPowerSaveTempWhitelistAppMethod != null && deviceIdleController != null) { Intent targetIntent = new Intent(intent);
int userId = (int) getUserIdMethod.invoke(null, getPackageManager().getApplicationInfo(packageName, 0).uid); targetIntent.setComponent(new ComponentName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name));
logd("Adding app " + packageName + " for userId " + userId + " to the temp whitelist"); if (resolveInfo.activityInfo.packageName.equals(packageName)) {
addPowerSaveTempWhitelistAppMethod.invoke(deviceIdleController, packageName, 10000, userId, "GCM Push"); // Wake up the package itself
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && app.wakeForDelivery) {
try {
if (getUserIdMethod != null && addPowerSaveTempWhitelistAppMethod != null && deviceIdleController != null) {
int userId = (int) getUserIdMethod.invoke(null, getPackageManager().getApplicationInfo(packageName, 0).uid);
logd("Adding app " + packageName + " for userId " + userId + " to the temp whitelist");
addPowerSaveTempWhitelistAppMethod.invoke(deviceIdleController, packageName, 10000, userId, "GCM Push");
}
} catch (Exception e) {
Log.w(TAG, e);
}
} }
} catch (Exception e) { // We don't need receiver permission for our own package
Log.w(TAG, e); logd("Deliver message to own receiver " + resolveInfo);
sendOrderedBroadcast(targetIntent, null);
} else if (resolveInfo.filter.hasCategory(packageName)) {
// Permission required
logd("Deliver message to third-party receiver (with permission check)" + resolveInfo);
sendOrderedBroadcast(targetIntent, receiverPermission);
} }
} }
targetIntent.setComponent(new ComponentName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name));
if (resolveInfo.activityInfo.packageName.equals(packageName)) {
sendOrderedBroadcast(targetIntent, null);
} else if (receiverPermission != null) {
sendOrderedBroadcast(targetIntent, receiverPermission);
} else {
Log.w(TAG, resolveInfo.activityInfo.packageName + "/" + resolveInfo.activityInfo.name + " matches for C2D message to " + packageName + " but corresponding permission was not declared");
}
} }
} }
} }
@ -569,6 +602,7 @@ public class McsService extends Service implements Handler.Callback {
for (AppData appData : msg.app_data) { for (AppData appData : msg.app_data) {
if (IDLE_NOTIFICATION.equals(appData.key)) { if (IDLE_NOTIFICATION.equals(appData.key)) {
DataMessageStanza.Builder msgResponse = new DataMessageStanza.Builder() DataMessageStanza.Builder msgResponse = new DataMessageStanza.Builder()
.id(Integer.toHexString(nextMessageId.incrementAndGet()))
.from(FROM_FIELD) .from(FROM_FIELD)
.sent(System.currentTimeMillis() / 1000) .sent(System.currentTimeMillis() / 1000)
.ttl(0) .ttl(0)
@ -633,6 +667,22 @@ public class McsService extends Service implements Handler.Callback {
scheduleReconnect(this); scheduleReconnect(this);
} }
return true; return true;
case MSG_ACK:
logd("Ack initiated, reason: " + msg.obj);
if (isConnected()) {
IqStanza.Builder iq = new IqStanza.Builder()
.type(IqStanza.IqType.SET)
.id("")
.extension(new Extension.Builder().id(13).data(ByteString.EMPTY).build()) // StreamAck
.status(0L);
if (inputStream.newStreamIdAvailable()) {
iq.last_stream_id_received(inputStream.getStreamId());
}
send(MCS_IQ_STANZA_TAG, iq.build());
} else {
logd("Ignoring ack, not connected!");
}
return true;
case MSG_OUTPUT_READY: case MSG_OUTPUT_READY:
logd("Sending login request..."); logd("Sending login request...");
send(MCS_LOGIN_REQUEST_TAG, buildLoginRequest()); send(MCS_LOGIN_REQUEST_TAG, buildLoginRequest());

View File

@ -28,13 +28,17 @@ import android.os.RemoteException;
import android.util.Log; import android.util.Log;
import org.microg.gms.checkin.LastCheckinInfo; import org.microg.gms.checkin.LastCheckinInfo;
import org.microg.gms.common.ForegroundServiceContext;
import org.microg.gms.common.PackageUtils; import org.microg.gms.common.PackageUtils;
import org.microg.gms.common.Utils; import org.microg.gms.common.Utils;
import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_REGISTRATION; import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_REGISTRATION;
import static org.microg.gms.gcm.GcmConstants.ERROR_SERVICE_NOT_AVAILABLE; import static org.microg.gms.gcm.GcmConstants.ERROR_SERVICE_NOT_AVAILABLE;
import static org.microg.gms.gcm.GcmConstants.EXTRA_APP; import static org.microg.gms.gcm.GcmConstants.EXTRA_APP;
import static org.microg.gms.gcm.GcmConstants.EXTRA_APP_OVERRIDE;
import static org.microg.gms.gcm.GcmConstants.EXTRA_ERROR; import static org.microg.gms.gcm.GcmConstants.EXTRA_ERROR;
import static org.microg.gms.gcm.McsConstants.ACTION_ACK;
import static org.microg.gms.gcm.McsConstants.ACTION_SEND;
class PushRegisterHandler extends Handler { class PushRegisterHandler extends Handler {
private static final String TAG = "GmsGcmRegisterHdl"; private static final String TAG = "GmsGcmRegisterHdl";
@ -54,40 +58,54 @@ class PushRegisterHandler extends Handler {
return super.sendMessageAtTime(msg, uptimeMillis); return super.sendMessageAtTime(msg, uptimeMillis);
} }
private void sendReply(int what, int id, Messenger replyTo, Bundle data) { private void sendReplyViaMessage(int what, int id, Messenger replyTo, Bundle messageData) {
if (what == 0) { Message response = Message.obtain();
Intent outIntent = new Intent(ACTION_C2DM_REGISTRATION); response.what = what;
outIntent.putExtras(data); response.arg1 = id;
Message message = Message.obtain(); response.setData(messageData);
message.obj = outIntent; try {
try { replyTo.send(response);
replyTo.send(message); } catch (RemoteException e) {
} catch (RemoteException e) { Log.w(TAG, e);
Log.w(TAG, e);
}
} else {
Bundle messageData = new Bundle();
messageData.putBundle("data", data);
Message response = Message.obtain();
response.what = what;
response.arg1 = id;
response.setData(messageData);
try {
replyTo.send(response);
} catch (RemoteException e) {
Log.w(TAG, e);
}
} }
} }
private void replyError(int what, int id, Messenger replyTo, String errorMessage) { private void sendReplyViaIntent(Intent outIntent, Messenger replyTo) {
Message message = Message.obtain();
message.obj = outIntent;
try {
replyTo.send(message);
} catch (RemoteException e) {
Log.w(TAG, e);
}
}
private void sendReply(int what, int id, Messenger replyTo, Bundle data, boolean oneWay) {
if (what == 0) {
Intent outIntent = new Intent(ACTION_C2DM_REGISTRATION);
outIntent.putExtras(data);
sendReplyViaIntent(outIntent, replyTo);
return;
}
Bundle messageData = new Bundle();
messageData.putBundle("data", data);
sendReplyViaMessage(what, id, replyTo, messageData);
}
private void replyError(int what, int id, Messenger replyTo, String errorMessage, boolean oneWay) {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putString(EXTRA_ERROR, errorMessage); bundle.putString(EXTRA_ERROR, errorMessage);
sendReply(what, id, replyTo, bundle); sendReply(what, id, replyTo, bundle, oneWay);
} }
private void replyNotAvailable(int what, int id, Messenger replyTo) { private void replyNotAvailable(int what, int id, Messenger replyTo) {
replyError(what, id, replyTo, ERROR_SERVICE_NOT_AVAILABLE); replyError(what, id, replyTo, ERROR_SERVICE_NOT_AVAILABLE, false);
}
private PendingIntent getSelfAuthIntent() {
Intent intent = new Intent();
intent.setPackage("com.google.example.invalidpackage");
return PendingIntent.getBroadcast(context, 0, intent, 0);
} }
@Override @Override
@ -119,15 +137,9 @@ class PushRegisterHandler extends Handler {
return; return;
} }
Bundle data = msg.getData(); Bundle data = msg.getData();
if (data.getBoolean("oneWay", false)) {
Log.w(TAG, "oneWay requested");
return;
}
String packageName = data.getString("pkg"); String packageName = data.getString("pkg");
Bundle subdata = data.getBundle("data"); Bundle subdata = data.getBundle("data");
String sender = subdata.getString("sender");
boolean delete = subdata.get("delete") != null;
try { try {
PackageUtils.checkPackageUid(context, packageName, callingUid); PackageUtils.checkPackageUid(context, packageName, callingUid);
@ -136,16 +148,46 @@ class PushRegisterHandler extends Handler {
return; return;
} }
// TODO: We should checkin and/or ask for permission here. Log.d(TAG, "handleMessage: package=" + packageName + " what=" + what + " id=" + id);
PushRegisterManager.completeRegisterRequest(context, database, boolean oneWay = data.getBoolean("oneWay", false);
new RegisterRequest()
.build(Utils.getBuild(context)) switch (what) {
.sender(sender) case 0:
.checkin(LastCheckinInfo.read(context)) case 1:
.app(packageName) // TODO: We should checkin and/or ask for permission here.
.delete(delete) String sender = subdata.getString("sender");
.extraParams(subdata), boolean delete = subdata.get("delete") != null;
bundle -> sendReply(what, id, replyTo, bundle));
PushRegisterManager.completeRegisterRequest(context, database,
new RegisterRequest()
.build(Utils.getBuild(context))
.sender(sender)
.checkin(LastCheckinInfo.read(context))
.app(packageName)
.delete(delete)
.extraParams(subdata),
bundle -> sendReply(what, id, replyTo, bundle, oneWay));
break;
case 2:
String messageId = subdata.getString("google.message_id");
Log.d(TAG, "Ack " + messageId + " for " + packageName);
Intent i = new Intent(context, McsService.class);
i.setAction(ACTION_ACK);
i.putExtra(EXTRA_APP, getSelfAuthIntent());
new ForegroundServiceContext(context).startService(i);
break;
default:
Bundle bundle = new Bundle();
bundle.putBoolean("unsupported", true);
sendReplyViaMessage(what, id, replyTo, bundle);
return;
}
if (oneWay) {
Bundle bundle = new Bundle();
bundle.putBoolean("ack", true);
sendReplyViaMessage(what, id, replyTo, bundle);
}
} }
} }