Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 93c3cbb3 authored by Marvin W.'s avatar Marvin W. 🐿️
Browse files

Add basic MCS implementation to realize C2DM (used by GCM)

- Can be started using `am startservice com.google.android.gms/org.microg.gms.gcm.mcs.McsService` on device
- Does not send Heartbeats -> Connection will be closed after 30 minutes
- No automatic reconnect on connection closure
- Only push, no bidirectional communication
parent b150e733
Loading
Loading
Loading
Loading
+28 −0
Original line number Diff line number Diff line
/*
 * Copyright 2013-2015 µg Project Team
 *
 * 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 org.microg.gms.gcm.mcs;

public class Constants {
    public static final int MCS_HEARTBEAT_ACK_TAG = 1;
    public static final int MCS_LOGIN_REQUEST_TAG = 2;
    public static final int MCS_LOGIN_RESPONSE_TAG = 3;
    public static final int MCS_CLOSE_TAG = 4;
    public static final int MCS_IQ_STANZA_TAG = 7;
    public static final int MCS_DATA_MESSAGE_STANZA_TAG = 8;

    public static final int MCS_VERSION_CODE = 41;
}
+127 −0
Original line number Diff line number Diff line
/*
 * Copyright 2013-2015 µg Project Team
 *
 * 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 org.microg.gms.gcm.mcs;

import android.util.Base64;
import android.util.Log;

import com.squareup.wire.Message;
import com.squareup.wire.Wire;

import java.io.IOException;
import java.io.InputStream;

import static org.microg.gms.gcm.mcs.Constants.MCS_CLOSE_TAG;
import static org.microg.gms.gcm.mcs.Constants.MCS_DATA_MESSAGE_STANZA_TAG;
import static org.microg.gms.gcm.mcs.Constants.MCS_HEARTBEAT_ACK_TAG;
import static org.microg.gms.gcm.mcs.Constants.MCS_IQ_STANZA_TAG;
import static org.microg.gms.gcm.mcs.Constants.MCS_LOGIN_REQUEST_TAG;
import static org.microg.gms.gcm.mcs.Constants.MCS_LOGIN_RESPONSE_TAG;

public class McsInputStream {
    private static final String TAG = "GmsGcmMcsInput";

    private final InputStream is;
    private boolean initialized;
    private int version = -1;
    private int lastStreamIdReported = -1;
    private int streamId = 0;

    public McsInputStream(InputStream is) {
        this(is, false);
    }

    public McsInputStream(InputStream is, boolean initialized) {
        this.is = is;
        this.initialized = initialized;
    }

    public int getStreamId() {
        lastStreamIdReported = streamId;
        return streamId;
    }

    public boolean newStreamIdAvailable() {
        return lastStreamIdReported != streamId;
    }

    public int getVersion() {
        ensureVersionRead();
        return version;
    }

    private void ensureVersionRead() {
        if (!initialized) {
            try {
                version = is.read();
                Log.d(TAG, "Reading from MCS version=" + version);
                initialized = true;
            } catch (IOException e) {
                Log.w(TAG, e);
            }
        }
    }

    public synchronized Message read() throws IOException {
        ensureVersionRead();
        int mcsTag = is.read();
        int mcsSize = readVarint();
        Log.d(TAG, "Reading from MCS tag=" + mcsTag + " size=" + mcsSize);
        byte[] bytes = new byte[mcsSize];
        int len = 0;
        while ((len += is.read(bytes, len, mcsSize - len)) < mcsSize) ;
        Log.d(TAG, "Reading from MCS: " + Base64.encodeToString(bytes, 0));
        Message read = read(mcsTag, bytes, len);
        Log.d(TAG, "Read from MCS: " + read);
        streamId++;
        return read;
    }

    private static Message read(int mcsTag, byte[] bytes, int len) throws IOException {
        Wire wire = new Wire();
        switch (mcsTag) {
            case MCS_HEARTBEAT_ACK_TAG:
                return wire.parseFrom(bytes, 0, len, HeartbeatAck.class);
            case MCS_LOGIN_REQUEST_TAG:
                return wire.parseFrom(bytes, 0, len, LoginRequest.class);
            case MCS_LOGIN_RESPONSE_TAG:
                return wire.parseFrom(bytes, 0, len, LoginResponse.class);
            case MCS_CLOSE_TAG:
                return wire.parseFrom(bytes, 0, len, Close.class);
            case MCS_IQ_STANZA_TAG:
                return wire.parseFrom(bytes, 0, len, IqStanza.class);
            case MCS_DATA_MESSAGE_STANZA_TAG:
                return wire.parseFrom(bytes, 0, len, DataMessageStanza.class);
            default:
                return null;
        }
    }

    private int readVarint() throws IOException {
        int res = 0;
        int s = 0;
        int b = 0x80;
        while ((b & 0x80) == 0x80) {
            b = is.read();
            res |= (b & 0x7F) << s;
            s += 7;
        }
        return res;
    }


}
+86 −0
Original line number Diff line number Diff line
/*
 * Copyright 2013-2015 µg Project Team
 *
 * 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 org.microg.gms.gcm.mcs;

import android.util.Log;

import com.squareup.wire.Message;

import java.io.IOException;
import java.io.OutputStream;

import static org.microg.gms.gcm.mcs.Constants.*;

public class McsOutputStream {
    private static final String TAG = "GmsGcmMcsOutput";

    private final OutputStream os;
    private boolean initialized;
    private int version = MCS_VERSION_CODE;
    private int streamId = 0;

    public McsOutputStream(OutputStream os) {
        this(os, false);
    }

    public McsOutputStream(OutputStream os, boolean initialized) {
        this.os = os;
        this.initialized = initialized;
    }

    public int getStreamId() {
        return streamId;
    }

    public void write(DataMessageStanza message) throws IOException {
        write(message, MCS_DATA_MESSAGE_STANZA_TAG);
    }

    public void write(LoginRequest loginRequest) throws IOException {
        write(loginRequest, MCS_LOGIN_REQUEST_TAG);
    }

    public void write(HeartbeatAck ack) throws IOException {
        write(ack, MCS_HEARTBEAT_ACK_TAG);
    }

    public synchronized void write(Message message, int tag) throws IOException {
        if (!initialized) {
            Log.d(TAG, "Write MCS version code: " + version);
            os.write(version);
            initialized = true;
        }
        Log.d(TAG, "Write to MCS: " + message);
        os.write(tag);
        writeVarint(os, message.getSerializedSize());
        os.write(message.toByteArray());
        os.flush();
        streamId++;
    }

    private void writeVarint(OutputStream os, int value) throws IOException {
        while (true) {
            if ((value & ~0x7FL) == 0) {
                os.write(value);
                return;
            } else {
                os.write((value & 0x7F) | 0x80);
                value >>>= 7;
            }
        }
    }
}
+196 −0
Original line number Diff line number Diff line
/*
 * Copyright 2013-2015 µg Project Team
 *
 * 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 org.microg.gms.gcm.mcs;

import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.util.Log;

import com.squareup.wire.Message;

import org.microg.gms.checkin.LastCheckinInfo;

import java.io.IOException;
import java.net.Socket;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.net.ssl.SSLContext;

import static android.os.Build.VERSION.SDK_INT;

public class McsService extends IntentService {
    private static final String TAG = "GmsGcmMcsSvc";
    public static final String PREFERENCES_NAME = "mcs";

    public static final String SERVICE_HOST = "mtalk.google.com";
    public static final int SERVICE_PORT = 5228;
    public static final String PREF_LAST_PERSISTENT_ID = "last_persistent_id";
    public static final String SELF_CATEGORY = "com.google.android.gsf.gtalkservice";
    public static final String IDLE_NOTIFICATION = "IdleNotification";
    public static final String FROM_FIELD = "gcm@android.com";
    private static AtomicBoolean connected = new AtomicBoolean(false);

    private Socket socket;
    private Socket sslSocket;
    private McsInputStream inputStream;
    private McsOutputStream outputStream;

    public McsService() {
        super(TAG);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        if (connected.compareAndSet(false, true)) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    connect();
                }
            }).start();
        } else {
            Log.d(TAG, "MCS connection already started");
        }
    }

    private void connect() {
        try {
            Log.d(TAG, "Starting MCS connection...");
            LastCheckinInfo info = LastCheckinInfo.read(this);
            socket = new Socket(SERVICE_HOST, SERVICE_PORT);
            Log.d(TAG, "Connected to " + SERVICE_HOST + ":" + SERVICE_PORT);
            sslSocket = SSLContext.getDefault().getSocketFactory().createSocket(socket, "mtalk.google.com", 5228, true);
            Log.d(TAG, "Activated SSL with " + SERVICE_HOST + ":" + SERVICE_PORT);
            inputStream = new McsInputStream(sslSocket.getInputStream());
            outputStream = new McsOutputStream(sslSocket.getOutputStream());
            LoginRequest loginRequest = buildLoginRequest(info);
            Log.d(TAG, "Sending login request...");
            outputStream.write(loginRequest);
            boolean close = false;
            while (!close) {
                Message o = inputStream.read();
                if (o instanceof DataMessageStanza) {
                    handleMessage((DataMessageStanza) o);
                } else if (o instanceof HeartbeatPing) {
                    handleHearbeatPing((HeartbeatPing) o);
                } else if (o instanceof Close) {
                    handleClose((Close) o);
                } else if (o instanceof LoginResponse) {
                    handleLoginresponse((LoginResponse) o);
                }
            }
            socket.close();
        } catch (Exception e) {
            Log.w(TAG, e);
            try {
                sslSocket.close();
            } catch (Exception ignored) {
            }
        }
        connected.set(false);
    }

    private void handleClose(Close close) throws IOException {
        throw new IOException("Server requested close!");
    }

    private void handleLoginresponse(LoginResponse loginResponse) throws IOException {
        getSharedPreferences().edit().putString(PREF_LAST_PERSISTENT_ID, null);
        if (loginResponse.error == null) {
            Log.d(TAG, "Logged in");
        } else {
            throw new IOException("Could not login: " + loginResponse.error);
        }
    }

    private void handleMessage(DataMessageStanza message) throws IOException {
        if (message.persistent_id != null) {
            String old = getSharedPreferences().getString(PREF_LAST_PERSISTENT_ID, null);
            if (old == null) {
                old = "";
            } else {
                old += "|";
            }
            getSharedPreferences().edit()
                    .putString(PREF_LAST_PERSISTENT_ID, old + message.persistent_id).apply();
        }
        if (SELF_CATEGORY.equals(message.category)) {
            handleSelfMessage(message);
        } else {
            handleAppMessage(message);
        }
    }

    private void handleHearbeatPing(HeartbeatPing ping) throws IOException {
        HeartbeatAck.Builder ack = new HeartbeatAck.Builder().status(ping.status);
        if (inputStream.newStreamIdAvailable()) {
            ack.last_stream_id_received(inputStream.getStreamId());
        }
        outputStream.write(ack.build());
    }

    private LoginRequest buildLoginRequest(LastCheckinInfo info) {
        return new LoginRequest.Builder()
                .adaptive_heartbeat(false)
                .auth_service(LoginRequest.AuthService.ANDROID_ID)
                .auth_token(Long.toString(info.securityToken))
                .id("android-" + SDK_INT)
                .domain("mcs.android.com")
                .device_id("android-" + Long.toHexString(info.androidId))
                .network_type(1)
                .resource(Long.toString(info.androidId))
                .user(Long.toString(info.androidId))
                .use_rmq2(true)
                .setting(Arrays.asList(new Setting("new_vc", "1")))
                .received_persistent_id(Arrays.asList(getSharedPreferences().getString(PREF_LAST_PERSISTENT_ID, "").split("\\|")))
                .build();
    }

    private void handleAppMessage(DataMessageStanza msg) {
        Intent intent = new Intent();
        intent.setAction("com.google.android.c2dm.intent.RECEIVE");
        intent.addCategory(msg.category);
        for (AppData appData : msg.app_data) {
            intent.putExtra(appData.key, appData.value);
        }
        sendOrderedBroadcast(intent, msg.category + ".permission.C2D_MESSAGE");
    }

    private void handleSelfMessage(DataMessageStanza msg) throws IOException {
        for (AppData appData : msg.app_data) {
            if (IDLE_NOTIFICATION.equals(appData.key)) {
                DataMessageStanza.Builder msgResponse = new DataMessageStanza.Builder()
                        .from(FROM_FIELD)
                        .sent(System.currentTimeMillis() / 1000)
                        .ttl(0)
                        .category(SELF_CATEGORY)
                        .app_data(Arrays.asList(new AppData(IDLE_NOTIFICATION, "false")));
                if (inputStream.newStreamIdAvailable()) {
                    msgResponse.last_stream_id_received(inputStream.getStreamId());
                }
                outputStream.write(msgResponse.build());
            }
        }
    }

    private SharedPreferences getSharedPreferences() {
        return getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
    }
}
+83 −0
Original line number Diff line number Diff line
// Code generated by Wire protocol buffer compiler, do not edit.
// Source file: protos-repo/mcs.proto
package org.microg.gms.gcm.mcs;

import com.squareup.wire.Message;
import com.squareup.wire.ProtoField;

import static com.squareup.wire.Message.Datatype.STRING;
import static com.squareup.wire.Message.Label.REQUIRED;

public final class AppData extends Message {

  public static final String DEFAULT_KEY = "";
  public static final String DEFAULT_VALUE = "";

  @ProtoField(tag = 1, type = STRING, label = REQUIRED)
  public final String key;

  @ProtoField(tag = 2, type = STRING, label = REQUIRED)
  public final String value;

  public AppData(String key, String value) {
    this.key = key;
    this.value = value;
  }

  private AppData(Builder builder) {
    this(builder.key, builder.value);
    setBuilder(builder);
  }

  @Override
  public boolean equals(Object other) {
    if (other == this) return true;
    if (!(other instanceof AppData)) return false;
    AppData o = (AppData) other;
    return equals(key, o.key)
        && equals(value, o.value);
  }

  @Override
  public int hashCode() {
    int result = hashCode;
    if (result == 0) {
      result = key != null ? key.hashCode() : 0;
      result = result * 37 + (value != null ? value.hashCode() : 0);
      hashCode = result;
    }
    return result;
  }

  public static final class Builder extends Message.Builder<AppData> {

    public String key;
    public String value;

    public Builder() {
    }

    public Builder(AppData message) {
      super(message);
      if (message == null) return;
      this.key = message.key;
      this.value = message.value;
    }

    public Builder key(String key) {
      this.key = key;
      return this;
    }

    public Builder value(String value) {
      this.value = value;
      return this;
    }

    @Override
    public AppData build() {
      checkRequiredFields();
      return new AppData(this);
    }
  }
}
Loading