Ensure checkin before gcm registration, fix gcm permission requests

This commit is contained in:
Marvin W 2020-10-15 22:08:05 +02:00
parent b0e52b7a89
commit 26f2e859b8
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
10 changed files with 426 additions and 518 deletions

View File

@ -198,8 +198,6 @@
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.AIRPLANE_MODE" />
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
<action android:name="android.net.conn.BACKGROUND_DATA_SETTING_CHANGED" />
<action android:name="android.server.checkin.CHECKIN" />
@ -478,7 +476,7 @@
android:name="org.microg.gms.ui.AskPushPermission"
android:excludeFromRecents="true"
android:process=":ui"
android:theme="@style/Theme.AppCompat.DayNight.Dialog.Alert" />
android:theme="@style/Theme.AppCompat.DayNight.Dialog.Alert.NoActionBar" />
<activity
android:name="org.microg.gms.ui.AboutFragment$AsActivity"

View File

@ -18,14 +18,16 @@ package org.microg.gms.checkin;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.app.AlarmManager;
import android.app.IntentService;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.os.ResultReceiver;
import android.util.Log;
import androidx.legacy.content.WakefulBroadcastReceiver;
@ -39,11 +41,15 @@ import org.microg.gms.people.PeopleManager;
public class CheckinService extends IntentService {
private static final String TAG = "GmsCheckinSvc";
public static final long MAX_VALID_CHECKIN_AGE = 24 * 60 * 60 * 1000; // 12 hours
public static final long REGULAR_CHECKIN_INTERVAL = 12 * 60 * 60 * 1000; // 12 hours
public static final long BACKUP_CHECKIN_DELAY = 3 * 60 * 60 * 1000; // 3 hours
public static final String BIND_ACTION = "com.google.android.gms.checkin.BIND_TO_SERVICE";
public static final String EXTRA_FORCE_CHECKIN = "force";
@Deprecated
public static final String EXTRA_CALLBACK_INTENT = "callback";
public static final String EXTRA_RESULT_RECEIVER = "receiver";
public static final String EXTRA_NEW_CHECKIN_TIME = "checkin_time";
private ICheckinService iface = new ICheckinService.Stub() {
@Override
@ -73,6 +79,14 @@ public class CheckinService extends IntentService {
if (intent.hasExtra(EXTRA_CALLBACK_INTENT)) {
startService((Intent) intent.getParcelableExtra(EXTRA_CALLBACK_INTENT));
}
if (intent.hasExtra(EXTRA_RESULT_RECEIVER)) {
ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
if (receiver != null) {
Bundle bundle = new Bundle();
bundle.putLong(EXTRA_NEW_CHECKIN_TIME, info.lastCheckin);
receiver.send(Activity.RESULT_OK, bundle);
}
}
}
}
} catch (Exception e) {

View File

@ -31,20 +31,20 @@ import static org.microg.gms.checkin.CheckinService.REGULAR_CHECKIN_INTERVAL;
public class TriggerReceiver extends WakefulBroadcastReceiver {
private static final String TAG = "GmsCheckinTrigger";
private static boolean registered = false;
@Override
public void onReceive(Context context, Intent intent) {
try {
boolean force = "android.provider.Telephony.SECRET_CODE".equals(intent.getAction());
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (CheckinPrefs.get(context).isEnabled() || force) {
if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction()) &&
LastCheckinInfo.read(context).lastCheckin > System.currentTimeMillis() - REGULAR_CHECKIN_INTERVAL) {
if (LastCheckinInfo.read(context).lastCheckin > System.currentTimeMillis() - REGULAR_CHECKIN_INTERVAL && !force) {
CheckinService.schedule(context);
return;
}
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = cm.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isConnected() || force) {
Intent subIntent = new Intent(context, CheckinService.class);

View File

@ -1,193 +0,0 @@
/*
* Copyright (C) 2018 microG 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;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.Log;
import org.microg.gms.checkin.LastCheckinInfo;
import org.microg.gms.common.ForegroundServiceContext;
import org.microg.gms.common.PackageUtils;
import org.microg.gms.common.Utils;
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.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.McsConstants.ACTION_ACK;
import static org.microg.gms.gcm.McsConstants.ACTION_SEND;
class PushRegisterHandler extends Handler {
private static final String TAG = "GmsGcmRegisterHdl";
private Context context;
private int callingUid;
private GcmDatabase database;
public PushRegisterHandler(Context context, GcmDatabase database) {
this.context = context;
this.database = database;
}
@Override
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
this.callingUid = Binder.getCallingUid();
return super.sendMessageAtTime(msg, uptimeMillis);
}
private void sendReplyViaMessage(int what, int id, Messenger replyTo, Bundle messageData) {
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 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.putString(EXTRA_ERROR, errorMessage);
sendReply(what, id, replyTo, bundle, oneWay);
}
private void replyNotAvailable(int what, int id, Messenger replyTo) {
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
public void handleMessage(Message msg) {
if (msg.what == 0) {
if (msg.obj instanceof Intent) {
Message nuMsg = Message.obtain();
nuMsg.what = msg.what;
nuMsg.arg1 = 0;
nuMsg.replyTo = null;
PendingIntent pendingIntent = ((Intent) msg.obj).getParcelableExtra(EXTRA_APP);
String packageName = PackageUtils.packageFromPendingIntent(pendingIntent);
Bundle data = new Bundle();
data.putBoolean("oneWay", false);
data.putString("pkg", packageName);
data.putBundle("data", msg.getData());
nuMsg.setData(data);
msg = nuMsg;
} else {
return;
}
}
int what = msg.what;
int id = msg.arg1;
Messenger replyTo = msg.replyTo;
if (replyTo == null) {
Log.w(TAG, "replyTo is null");
return;
}
Bundle data = msg.getData();
String packageName = data.getString("pkg");
Bundle subdata = data.getBundle("data");
try {
PackageUtils.checkPackageUid(context, packageName, callingUid);
} catch (SecurityException e) {
Log.w(TAG, e);
return;
}
Log.d(TAG, "handleMessage: package=" + packageName + " what=" + what + " id=" + id);
boolean oneWay = data.getBoolean("oneWay", false);
switch (what) {
case 0:
case 1:
// TODO: We should checkin and/or ask for permission here.
String sender = subdata.getString("sender");
boolean delete = subdata.get("delete") != null;
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);
}
}
}

View File

@ -1,41 +0,0 @@
/*
* Copyright (C) 2018 microG 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;
import android.content.Context;
import android.content.Intent;
import androidx.legacy.content.WakefulBroadcastReceiver;
import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_REGISTER;
import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_UNREGISTER;
public class PushRegisterReceiver extends WakefulBroadcastReceiver {
private static final String TAG = "GmsGcmRegisterRcv";
@Override
public void onReceive(Context context, Intent intent) {
Intent intent2 = new Intent(context, PushRegisterService.class);
if (intent.getExtras().get("delete") != null) {
intent2.setAction(ACTION_C2DM_UNREGISTER);
} else {
intent2.setAction(ACTION_C2DM_REGISTER);
}
intent2.putExtras(intent.getExtras());
startWakefulService(context, intent2);
}
}

View File

@ -1,200 +0,0 @@
/*
* Copyright (C) 2013-2017 microG 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;
import android.app.IntentService;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.legacy.content.WakefulBroadcastReceiver;
import org.microg.gms.checkin.CheckinService;
import org.microg.gms.checkin.LastCheckinInfo;
import org.microg.gms.common.PackageUtils;
import org.microg.gms.common.Utils;
import org.microg.gms.ui.AskPushPermission;
import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_REGISTER;
import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_REGISTRATION;
import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_UNREGISTER;
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_DELETE;
import static org.microg.gms.gcm.GcmConstants.EXTRA_ERROR;
import static org.microg.gms.gcm.GcmConstants.EXTRA_KID;
import static org.microg.gms.gcm.GcmConstants.EXTRA_MESSENGER;
import static org.microg.gms.gcm.GcmConstants.EXTRA_PENDING_INTENT;
import static org.microg.gms.gcm.GcmConstants.EXTRA_SENDER;
public class PushRegisterService extends IntentService {
private static final String TAG = "GmsGcmRegisterSvc";
private static final String EXTRA_SKIP_TRY_CHECKIN = "skip_checkin";
private GcmDatabase database;
private static boolean requestPending = false;
public PushRegisterService() {
super(TAG);
setIntentRedelivery(false);
}
@Override
public void onCreate() {
super.onCreate();
database = new GcmDatabase(this);
}
@Override
public void onDestroy() {
super.onDestroy();
database.close();
}
@Override
protected void onHandleIntent(Intent intent) {
WakefulBroadcastReceiver.completeWakefulIntent(intent);
Log.d(TAG, "onHandleIntent: " + intent);
String requestId = null;
if (intent.hasExtra(EXTRA_KID) && intent.getStringExtra(EXTRA_KID).startsWith("|")) {
String[] kid = intent.getStringExtra(EXTRA_KID).split("\\|");
if (kid.length >= 3 && "ID".equals(kid[1])) {
requestId = kid[2];
}
}
if (LastCheckinInfo.read(this).lastCheckin > 0) {
try {
if (ACTION_C2DM_UNREGISTER.equals(intent.getAction()) ||
(ACTION_C2DM_REGISTER.equals(intent.getAction()) && "1".equals(intent.getStringExtra(EXTRA_DELETE)))) {
unregister(intent, requestId);
} else if (ACTION_C2DM_REGISTER.equals(intent.getAction())) {
register(intent, requestId);
}
} catch (Exception e) {
Log.w(TAG, e);
}
} else if (!intent.getBooleanExtra(EXTRA_SKIP_TRY_CHECKIN, false)) {
Log.d(TAG, "No checkin yet, trying to checkin");
intent.putExtra(EXTRA_SKIP_TRY_CHECKIN, true);
Intent subIntent = new Intent(this, CheckinService.class);
subIntent.putExtra(CheckinService.EXTRA_FORCE_CHECKIN, true);
subIntent.putExtra(CheckinService.EXTRA_CALLBACK_INTENT, intent);
startService(subIntent);
}
}
private void register(final Intent intent, String requestId) {
PendingIntent pendingIntent = intent.getParcelableExtra(EXTRA_APP);
final String packageName = PackageUtils.packageFromPendingIntent(pendingIntent);
GcmDatabase.App app = database.getApp(packageName);
if (app == null && GcmPrefs.get(this).isConfirmNewApps()) {
try {
getPackageManager().getApplicationInfo(packageName, 0); // Check package exists
Intent i = new Intent(this, AskPushPermission.class);
i.putExtra(EXTRA_PENDING_INTENT, intent);
i.putExtra(EXTRA_APP, packageName);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
} catch (PackageManager.NameNotFoundException e) {
replyNotAvailable(this, intent, packageName, requestId);
}
} else {
registerAndReply(this, database, intent, packageName, requestId);
}
}
public static void replyNotAvailable(Context context, Intent intent, String packageName, String requestId) {
Intent outIntent = new Intent(ACTION_C2DM_REGISTRATION);
outIntent.putExtra(EXTRA_ERROR, PushRegisterManager.attachRequestId(ERROR_SERVICE_NOT_AVAILABLE, requestId));
sendReply(context, intent, packageName, outIntent);
}
public static void registerAndReply(Context context, GcmDatabase database, Intent intent, String packageName, String requestId) {
Log.d(TAG, "register[req]: " + intent.toString() + " extras=" + intent.getExtras());
PushRegisterManager.completeRegisterRequest(context, database,
new RegisterRequest()
.build(Utils.getBuild(context))
.sender(intent.getStringExtra(EXTRA_SENDER))
.checkin(LastCheckinInfo.read(context))
.app(packageName)
.extraParams(intent.getExtras()),
bundle -> {
Intent outIntent = new Intent(ACTION_C2DM_REGISTRATION);
outIntent.putExtras(bundle);
Log.d(TAG, "register[res]: " + outIntent.toString() + " extras=" + outIntent.getExtras());
sendReply(context, intent, packageName, outIntent);
});
}
private static void sendReply(Context context, Intent intent, String packageName, Intent outIntent) {
try {
if (intent != null && intent.hasExtra(EXTRA_MESSENGER)) {
Messenger messenger = intent.getParcelableExtra(EXTRA_MESSENGER);
Message message = Message.obtain();
message.obj = outIntent;
messenger.send(message);
return;
}
} catch (Exception e) {
Log.w(TAG, e);
}
outIntent.setPackage(packageName);
context.sendOrderedBroadcast(outIntent, null);
}
private void unregister(Intent intent, String requestId) {
PendingIntent pendingIntent = intent.getParcelableExtra(EXTRA_APP);
String packageName = PackageUtils.packageFromPendingIntent(pendingIntent);
Log.d(TAG, "unregister[req]: " + intent.toString() + " extras=" + intent.getExtras());
PushRegisterManager.completeRegisterRequest(this, database,
new RegisterRequest()
.build(Utils.getBuild(this))
.sender(intent.getStringExtra(EXTRA_SENDER))
.checkin(LastCheckinInfo.read(this))
.app(packageName)
.extraParams(intent.getExtras()),
bundle -> {
Intent outIntent = new Intent(ACTION_C2DM_REGISTRATION);
outIntent.putExtras(bundle);
Log.d(TAG, "unregister[res]: " + outIntent.toString() + " extras=" + outIntent.getExtras());
sendReply(this, intent, packageName, outIntent);
});
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
Log.d(TAG, "onBind: " + intent.toString());
if (ACTION_C2DM_REGISTER.equals(intent.getAction())) {
Messenger messenger = new Messenger(new PushRegisterHandler(this, database));
return messenger.getBinder();
}
return super.onBind(intent);
}
}

View File

@ -50,7 +50,6 @@ public class TriggerReceiver extends WakefulBroadcastReceiver {
}
}
@Override
public void onReceive(Context context, Intent intent) {
try {

View File

@ -1,9 +1,11 @@
package org.microg.gms.ui;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.text.Html;
import android.view.View;
import android.widget.TextView;
@ -20,13 +22,15 @@ import static org.microg.gms.gcm.GcmConstants.EXTRA_KID;
import static org.microg.gms.gcm.GcmConstants.EXTRA_PENDING_INTENT;
public class AskPushPermission extends FragmentActivity {
public static final String EXTRA_REQUESTED_PACKAGE = "package";
public static final String EXTRA_RESULT_RECEIVER = "receiver";
public static final String EXTRA_EXPLICIT = "explicit";
private GcmDatabase database;
private String packageName;
private Intent intent;
private ResultReceiver resultReceiver;
private boolean answered;
private String requestId;
@Override
public void onCreate(Bundle savedInstanceState) {
@ -34,18 +38,17 @@ public class AskPushPermission extends FragmentActivity {
database = new GcmDatabase(this);
packageName = getIntent().getStringExtra(EXTRA_APP);
intent = getIntent().getParcelableExtra(EXTRA_PENDING_INTENT);
requestId = null;
if (intent.hasExtra(EXTRA_KID) && intent.getStringExtra(EXTRA_KID).startsWith("|")) {
String[] kid = intent.getStringExtra(EXTRA_KID).split("\\|");
if (kid.length >= 3 && "ID".equals(kid[1])) {
requestId = kid[2];
}
packageName = getIntent().getStringExtra(EXTRA_REQUESTED_PACKAGE);
resultReceiver = getIntent().getParcelableExtra(EXTRA_RESULT_RECEIVER);
if (packageName == null || resultReceiver == null) {
answered = true;
finish();
return;
}
if (database.getApp(packageName) != null) {
resultReceiver.send(Activity.RESULT_OK, Bundle.EMPTY);
answered = true;
finish();
return;
}
@ -64,12 +67,9 @@ public class AskPushPermission extends FragmentActivity {
if (answered) return;
database.noteAppKnown(packageName, true);
answered = true;
new Thread(new Runnable() {
@Override
public void run() {
PushRegisterService.registerAndReply(AskPushPermission.this, database, intent, packageName, requestId);
}
}).start();
Bundle bundle = new Bundle();
bundle.putBoolean(EXTRA_EXPLICIT, true);
resultReceiver.send(Activity.RESULT_OK, bundle);
finish();
}
});
@ -79,7 +79,9 @@ public class AskPushPermission extends FragmentActivity {
if (answered) return;
database.noteAppKnown(packageName, false);
answered = true;
PushRegisterService.replyNotAvailable(AskPushPermission.this, intent, packageName, requestId);
Bundle bundle = new Bundle();
bundle.putBoolean(EXTRA_EXPLICIT, true);
resultReceiver.send(Activity.RESULT_CANCELED, bundle);
finish();
}
});
@ -92,8 +94,7 @@ public class AskPushPermission extends FragmentActivity {
protected void onDestroy() {
super.onDestroy();
if (!answered) {
PushRegisterService.replyNotAvailable(AskPushPermission.this, intent, packageName, requestId);
answered = true;
resultReceiver.send(Activity.RESULT_CANCELED, Bundle.EMPTY);
}
database.close();
}

View File

@ -0,0 +1,363 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.gcm
import android.app.Activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.*
import android.util.Log
import androidx.legacy.content.WakefulBroadcastReceiver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import org.microg.gms.checkin.CheckinPrefs
import org.microg.gms.checkin.CheckinService
import org.microg.gms.checkin.LastCheckinInfo
import org.microg.gms.common.ForegroundServiceContext
import org.microg.gms.common.PackageUtils
import org.microg.gms.common.Utils
import org.microg.gms.gcm.GcmConstants.*
import org.microg.gms.ui.AskPushPermission
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
private const val TAG = "GmsGcmRegister"
private suspend fun ensureCheckinIsUpToDate(context: Context) {
if (!CheckinPrefs.get(context).isEnabled) throw RuntimeException("Checkin disabled")
val lastCheckin = LastCheckinInfo.read(context).lastCheckin
if (lastCheckin < System.currentTimeMillis() - CheckinService.MAX_VALID_CHECKIN_AGE) {
val resultData: Bundle = suspendCoroutine { continuation ->
val intent = Intent(context, CheckinService::class.java)
val continued = AtomicBoolean(false)
intent.putExtra(CheckinService.EXTRA_RESULT_RECEIVER, object : ResultReceiver(null) {
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
if (continued.compareAndSet(false, true)) continuation.resume(resultData ?: Bundle.EMPTY)
}
})
ForegroundServiceContext(context).startService(intent)
Handler().postDelayed({
if (continued.compareAndSet(false, true)) continuation.resume(Bundle.EMPTY)
}, 10000L)
}
if (resultData.getLong(CheckinService.EXTRA_NEW_CHECKIN_TIME, 0L) + lastCheckin == 0L) {
throw RuntimeException("No checkin available")
}
}
}
private suspend fun ensureAppRegistrationAllowed(context: Context, database: GcmDatabase, packageName: String) {
if (!GcmPrefs.get(context).isEnabled) throw RuntimeException("GCM disabled")
val app = database.getApp(packageName)
if (app == null && GcmPrefs.get(context).isConfirmNewApps) {
val accepted: Boolean = suspendCoroutine { continuation ->
val i = Intent(context, AskPushPermission::class.java)
i.putExtra(AskPushPermission.EXTRA_REQUESTED_PACKAGE, packageName)
i.putExtra(AskPushPermission.EXTRA_RESULT_RECEIVER, object : ResultReceiver(null) {
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
continuation.resume(resultCode == Activity.RESULT_OK)
}
})
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
i.addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT)
context.startActivity(i)
}
if (!accepted) {
throw RuntimeException("Push permission not granted to app")
}
} else if (!app.allowRegister) {
throw RuntimeException("Push permission not granted to app")
}
}
suspend fun completeRegisterRequest(context: Context, database: GcmDatabase, request: RegisterRequest, requestId: String? = null): Bundle = suspendCoroutine { continuation ->
PushRegisterManager.completeRegisterRequest(context, database, requestId, request) { continuation.resume(it) }
}
private val Intent.requestId: String?
get() {
val kidString = getStringExtra(GcmConstants.EXTRA_KID) ?: return null
if (kidString.startsWith("|")) {
val kid = kidString.split("\\|".toRegex()).toTypedArray()
if (kid.size >= 3 && "ID" == kid[1]) {
return kid[2]
}
}
return null
}
private val Intent.app: PendingIntent?
get() = getParcelableExtra(EXTRA_APP)
private val Intent.appPackageName: String?
get() = PackageUtils.packageFromPendingIntent(app)
class PushRegisterService : LifecycleService() {
private lateinit var database: GcmDatabase
override fun onCreate() {
super.onCreate()
database = GcmDatabase(this)
}
override fun onDestroy() {
super.onDestroy()
database.close()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
WakefulBroadcastReceiver.completeWakefulIntent(intent)
Log.d(TAG, "onStartCommand: $intent")
lifecycleScope.launchWhenStarted {
if (intent == null) return@launchWhenStarted
handleIntent(intent)
}
return super.onStartCommand(intent, flags, startId)
}
private suspend fun handleIntent(intent: Intent) {
try {
ensureCheckinIsUpToDate(this)
if (ACTION_C2DM_UNREGISTER == intent.action || ACTION_C2DM_REGISTER == intent.action && "1" == intent.getStringExtra(EXTRA_DELETE)) {
unregister(intent)
} else if (ACTION_C2DM_REGISTER == intent.action) {
register(intent)
}
} catch (e: Exception) {
Log.w(TAG, e)
replyNotAvailable(intent)
}
}
private fun replyNotAvailable(intent: Intent) {
val outIntent = Intent(ACTION_C2DM_REGISTRATION)
outIntent.putExtra(EXTRA_ERROR, PushRegisterManager.attachRequestId(ERROR_SERVICE_NOT_AVAILABLE, intent.requestId))
sendReply(intent, intent.appPackageName, outIntent)
}
private suspend fun register(intent: Intent) {
val packageName = intent.appPackageName ?: throw RuntimeException("No package provided")
ensureAppRegistrationAllowed(this, database, packageName)
Log.d(TAG, "register[req]: " + intent.toString() + " extras=" + intent!!.extras)
val bundle = completeRegisterRequest(this, database,
RegisterRequest()
.build(Utils.getBuild(this))
.sender(intent.getStringExtra(EXTRA_SENDER))
.checkin(LastCheckinInfo.read(this))
.app(packageName)
.extraParams(intent.extras))
val outIntent = Intent(ACTION_C2DM_REGISTRATION)
outIntent.putExtras(bundle)
Log.d(TAG, "register[res]: " + outIntent.toString() + " extras=" + outIntent.extras)
sendReply(intent, packageName, outIntent)
}
private suspend fun unregister(intent: Intent) {
val packageName = intent.appPackageName ?: throw RuntimeException("No package provided")
Log.d(TAG, "unregister[req]: " + intent.toString() + " extras=" + intent.extras)
val bundle = completeRegisterRequest(this, database, RegisterRequest()
.build(Utils.getBuild(this))
.sender(intent.getStringExtra(EXTRA_SENDER))
.checkin(LastCheckinInfo.read(this))
.app(packageName)
.extraParams(intent.extras)
)
val outIntent = Intent(ACTION_C2DM_REGISTRATION)
outIntent.putExtras(bundle)
Log.d(TAG, "unregister[res]: " + outIntent.toString() + " extras=" + outIntent.extras)
sendReply(intent, packageName, outIntent)
}
private fun sendReply(intent: Intent, packageName: String?, outIntent: Intent) {
if (sendReplyToMessenger(intent, outIntent)) return
outIntent.setPackage(packageName)
sendOrderedBroadcast(outIntent, null)
}
private fun sendReplyToMessenger(intent: Intent, outIntent: Intent): Boolean {
try {
val messenger = intent.getParcelableExtra<Messenger>(EXTRA_MESSENGER) ?: return false
val message = Message.obtain()
message.obj = outIntent
messenger.send(message)
return true
} catch (e: Exception) {
Log.w(TAG, e)
return false
}
}
override fun onBind(intent: Intent): IBinder? {
Log.d(TAG, "onBind: $intent")
super.onBind(intent)
if (ACTION_C2DM_REGISTER == intent.action) {
val messenger = Messenger(PushRegisterHandler(this, database, lifecycle))
return messenger.binder
}
return null
}
}
internal class PushRegisterHandler(private val context: Context, private val database: GcmDatabase, private val lifecycle: Lifecycle) : Handler(), LifecycleOwner {
override fun getLifecycle(): Lifecycle = lifecycle
private var callingUid = 0
override fun sendMessageAtTime(msg: Message, uptimeMillis: Long): Boolean {
callingUid = Binder.getCallingUid()
return super.sendMessageAtTime(msg, uptimeMillis)
}
private fun sendReplyViaMessage(what: Int, id: Int, replyTo: Messenger, messageData: Bundle) {
val response = Message.obtain()
response.what = what
response.arg1 = id
response.data = messageData
try {
replyTo.send(response)
} catch (e: RemoteException) {
Log.w(TAG, e)
}
}
private fun sendReplyViaIntent(outIntent: Intent, replyTo: Messenger) {
val message = Message.obtain()
message.obj = outIntent
try {
replyTo.send(message)
} catch (e: RemoteException) {
Log.w(TAG, e)
}
}
private fun sendReply(what: Int, id: Int, replyTo: Messenger, data: Bundle, oneWay: Boolean) {
if (what == 0) {
val outIntent = Intent(ACTION_C2DM_REGISTRATION)
outIntent.putExtras(data)
sendReplyViaIntent(outIntent, replyTo)
return
}
val messageData = Bundle()
messageData.putBundle("data", data)
sendReplyViaMessage(what, id, replyTo, messageData)
}
private fun replyError(what: Int, id: Int, replyTo: Messenger, errorMessage: String, oneWay: Boolean) {
val bundle = Bundle()
bundle.putString(EXTRA_ERROR, errorMessage)
sendReply(what, id, replyTo, bundle, oneWay)
}
private fun replyNotAvailable(what: Int, id: Int, replyTo: Messenger) {
replyError(what, id, replyTo, ERROR_SERVICE_NOT_AVAILABLE, false)
}
private val selfAuthIntent: PendingIntent
private get() {
val intent = Intent()
intent.setPackage("com.google.example.invalidpackage")
return PendingIntent.getBroadcast(context, 0, intent, 0)
}
override fun handleMessage(msg: Message) {
var msg = msg
val obj = msg.obj
if (msg.what == 0) {
if (obj is Intent) {
val nuMsg = Message.obtain()
nuMsg.what = msg.what
nuMsg.arg1 = 0
nuMsg.replyTo = null
val packageName = obj.appPackageName
val data = Bundle()
data.putBoolean("oneWay", false)
data.putString("pkg", packageName)
data.putBundle("data", msg.data)
nuMsg.data = data
msg = nuMsg
} else {
return
}
}
val what = msg.what
val id = msg.arg1
val replyTo = msg.replyTo
if (replyTo == null) {
Log.w(TAG, "replyTo is null")
return
}
val data = msg.data
val packageName = data.getString("pkg") ?: return
val subdata = data.getBundle("data")
try {
PackageUtils.checkPackageUid(context, packageName, callingUid)
} catch (e: SecurityException) {
Log.w(TAG, e)
return
}
Log.d(TAG, "handleMessage: package=$packageName what=$what id=$id")
val oneWay = data.getBoolean("oneWay", false)
when (what) {
0, 1 -> {
lifecycleScope.launchWhenStarted {
try {
val sender = subdata?.getString("sender")
val delete = subdata?.get("delete") != null
ensureCheckinIsUpToDate(context)
if (!delete) ensureAppRegistrationAllowed(context, database, packageName)
val bundle = completeRegisterRequest(context, database,
RegisterRequest()
.build(Utils.getBuild(context))
.sender(sender)
.checkin(LastCheckinInfo.read(context))
.app(packageName)
.delete(delete)
.extraParams(subdata))
sendReply(what, id, replyTo, bundle, oneWay)
} catch (e: Exception) {
Log.w(TAG, e)
replyNotAvailable(what, id, replyTo)
}
}
}
2 -> {
val messageId = subdata!!.getString("google.message_id")
Log.d(TAG, "Ack $messageId for $packageName")
val i = Intent(context, McsService::class.java)
i.action = McsConstants.ACTION_ACK
i.putExtra(EXTRA_APP, selfAuthIntent)
ForegroundServiceContext(context).startService(i)
}
else -> {
val bundle = Bundle()
bundle.putBoolean("unsupported", true)
sendReplyViaMessage(what, id, replyTo, bundle)
return
}
}
if (oneWay) {
val bundle = Bundle()
bundle.putBoolean("ack", true)
sendReplyViaMessage(what, id, replyTo, bundle)
}
}
}
class PushRegisterReceiver : WakefulBroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val intent2 = Intent(context, PushRegisterService::class.java)
if (intent.extras!!["delete"] != null) {
intent2.action = ACTION_C2DM_UNREGISTER
} else {
intent2.action = ACTION_C2DM_REGISTER
}
intent2.putExtras(intent.extras!!)
startWakefulService(context, intent2)
}
}

View File

@ -31,11 +31,11 @@
android:id="@+id/desc_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingEnd="16dip"
android:paddingLeft="20dip"
android:paddingRight="16dip"
android:paddingStart="20dip"
android:paddingTop="18dip">
android:paddingLeft="20dip"
android:paddingTop="18dip"
android:paddingEnd="16dip"
android:paddingRight="16dip">
<LinearLayout
android:id="@+id/perm_desc_root"
@ -57,8 +57,8 @@
style="@style/TextAppearance.AppCompat.Subhead"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="16dip"
android:paddingStart="16dip"
android:paddingLeft="16dip"
android:text="Allow {appName} to register for push notifications?"
android:textSize="20sp">
</TextView>
@ -70,61 +70,28 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingEnd="16dip"
android:gravity="end"
android:orientation="horizontal"
android:paddingStart="20dip"
android:paddingLeft="20dip"
android:paddingRight="16dip"
android:paddingStart="20dip">
android:paddingEnd="16dip"
android:paddingRight="16dip">
<android.support.v7.widget.ButtonBarLayout
android:id="@+id/button_group"
android:layout_width="match_parent"
<Button
android:id="@+id/permission_deny_button"
style="?attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="bottom"
android:orientation="horizontal"
android:paddingBottom="4dp"
android:paddingLeft="6dip"
android:paddingStart="6dip"
android:paddingTop="4dp">
android:text="@string/deny">
</Button>
<TextView
android:id="@+id/current_page_text"
style="?android:attr/textAppearanceSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:maxLines="1"
android:paddingEnd="12dp"
android:paddingRight="12dp"
android:textColor="?android:attr/textColorSecondary"
android:visibility="invisible">
</TextView>
<android.support.v4.widget.Space
android:id="@*android:id/spacer"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"
android:visibility="invisible">
</android.support.v4.widget.Space>
<Button
android:id="@+id/permission_deny_button"
style="?attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/deny">
</Button>
<Button
android:id="@+id/permission_allow_button"
style="?attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/allow">
</Button>
</android.support.v7.widget.ButtonBarLayout>
<Button
android:id="@+id/permission_allow_button"
style="?attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/allow">
</Button>
</LinearLayout>