Refactor settings access to use a SettingsProvider

to enable safe usage of settings no matter which process is getting/setting them.
Previously, different processes were accessing settings in an unsafe way and the warn methods were throwing Runtime exceptions which went largely unnoticed, but happened, especially on a fresh start of the OS.

Change-Id: Ie4134e7be2a7ca4a373790f45fbcbd09bf02ad86
This commit is contained in:
Torsten Grote 2021-05-31 17:01:07 -03:00 committed by Marvin W
parent fbcb6066c3
commit aa3a2d65cd
31 changed files with 814 additions and 703 deletions

View File

@ -0,0 +1,120 @@
package org.microg.gms.settings
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.net.Uri
object SettingsContract {
const val AUTHORITY = "org.microg.gms.settings"
val AUTHORITY_URI: Uri = Uri.parse("content://$AUTHORITY")
object CheckIn {
private const val id = "check-in"
val CONTENT_URI: Uri = Uri.withAppendedPath(AUTHORITY_URI, id)
const val CONTENT_TYPE = "vnd.android.cursor.item/vnd.$AUTHORITY.$id"
const val ENABLED = "checkin_enable_service"
const val ANDROID_ID = "androidId"
const val DIGEST = "digest"
const val LAST_CHECK_IN = "lastCheckin"
const val SECURITY_TOKEN = "securityToken"
const val VERSION_INFO = "versionInfo"
const val DEVICE_DATA_VERSION_INFO = "deviceDataVersionInfo"
val PROJECTION = arrayOf(
ENABLED,
ANDROID_ID,
DIGEST,
LAST_CHECK_IN,
SECURITY_TOKEN,
VERSION_INFO,
DEVICE_DATA_VERSION_INFO,
)
const val PREFERENCES_NAME = "checkin"
const val INITIAL_DIGEST = "1-929a0dca0eee55513280171a8585da7dcd3700f8"
}
object Gcm {
private const val id = "gcm"
val CONTENT_URI: Uri = Uri.withAppendedPath(AUTHORITY_URI, id)
const val CONTENT_TYPE = "vnd.android.cursor.item/vnd.$AUTHORITY.$id"
const val FULL_LOG = "gcm_full_log"
const val LAST_PERSISTENT_ID = "gcm_last_persistent_id"
const val CONFIRM_NEW_APPS = "gcm_confirm_new_apps"
const val ENABLE_GCM = "gcm_enable_mcs_service"
const val NETWORK_MOBILE = "gcm_network_mobile"
const val NETWORK_WIFI = "gcm_network_wifi"
const val NETWORK_ROAMING = "gcm_network_roaming"
const val NETWORK_OTHER = "gcm_network_other"
const val LEARNT_MOBILE = "gcm_learnt_mobile"
const val LEARNT_WIFI = "gcm_learnt_wifi"
const val LEARNT_OTHER = "gcm_learnt_other"
val PROJECTION = arrayOf(
FULL_LOG,
LAST_PERSISTENT_ID,
CONFIRM_NEW_APPS,
ENABLE_GCM,
NETWORK_MOBILE,
NETWORK_WIFI,
NETWORK_ROAMING,
NETWORK_OTHER,
LEARNT_MOBILE,
LEARNT_WIFI,
LEARNT_OTHER,
)
}
object Auth {
private const val id = "auth"
val CONTENT_URI: Uri = Uri.withAppendedPath(AUTHORITY_URI, id)
const val CONTENT_TYPE = "vnd.android.cursor.item/vnd.$AUTHORITY.$id"
const val TRUST_GOOGLE = "auth_manager_trust_google"
const val VISIBLE = "auth_manager_visible"
val PROJECTION = arrayOf(
TRUST_GOOGLE,
VISIBLE,
)
}
object Exposure {
private const val id = "exposureNotification"
val CONTENT_URI: Uri = Uri.withAppendedPath(AUTHORITY_URI, id)
const val CONTENT_TYPE = "vnd.android.cursor.item/vnd.$AUTHORITY.$id"
const val SCANNER_ENABLED = "exposure_scanner_enabled"
const val LAST_CLEANUP = "exposure_last_cleanup"
val PROJECTION = arrayOf(
SCANNER_ENABLED,
LAST_CLEANUP,
)
}
object SafetyNet {
private const val id = "safety-net"
val CONTENT_URI: Uri = Uri.withAppendedPath(AUTHORITY_URI, id)
const val CONTENT_TYPE = "vnd.android.cursor.item/vnd.$AUTHORITY.$id"
}
fun <T> getSettings(context: Context, uri: Uri, projection: Array<out String>?, f: (Cursor) -> T): T {
context.contentResolver.query(uri, projection, null, null, null).use { c ->
require(c != null) { "Cursor for query $uri ${projection?.toList()} was null" }
if (!c.moveToFirst()) error("Cursor for query $uri ${projection?.toList()} was empty")
return f.invoke(c)
}
}
fun setSettings(context: Context, uri: Uri, v: ContentValues.() -> Unit) {
val values = ContentValues().apply { v.invoke(this) }
val affected = context.contentResolver.update(uri, values, null, null)
require(affected == 1) { "Update for $uri with $values affected 0 rows"}
}
}

View File

@ -129,6 +129,13 @@
android:exported="true"
android:permission="org.microg.gms.PROVISION" />
<!-- Internal Settings -->
<provider
android:name="org.microg.gms.settings.SettingsProvider"
android:authorities="org.microg.gms.settings"
android:exported="false" />
<!-- Location -->
<activity
@ -198,8 +205,6 @@
</intent-filter>
</service>
<receiver android:name="org.microg.gms.checkin.ServiceInfoReceiver" />
<receiver android:name="org.microg.gms.checkin.TriggerReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />

View File

@ -21,22 +21,22 @@ import android.accounts.AccountManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.preference.PreferenceManager;
import android.util.Log;
import org.microg.gms.common.PackageUtils;
import org.microg.gms.settings.SettingsContract;
import java.io.IOException;
import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
import static android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
import static org.microg.gms.auth.AuthPrefs.isTrustGooglePermitted;
public class AuthManager {
private static final String TAG = "GmsAuthManager";
public static final String PERMISSION_TREE_BASE = "com.google.android.googleapps.permission.GOOGLE_AUTH.";
private static final String PREF_AUTH_TRUST_GOOGLE = "auth_manager_trust_google";
public static final String PREF_AUTH_VISIBLE = "auth_manager_visible";
public static final String PREF_AUTH_VISIBLE = SettingsContract.Auth.VISIBLE;
public static final int ONE_HOUR_IN_SECONDS = 60 * 60;
private final Context context;
@ -185,14 +185,6 @@ public class AuthManager {
}
}
public static boolean isTrustGooglePermitted(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(PREF_AUTH_TRUST_GOOGLE, true);
}
public static boolean isAuthVisible(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(PREF_AUTH_VISIBLE, false);
}
private boolean isSystemApp() {
try {
int flags = context.getPackageManager().getApplicationInfo(packageName, 0).flags;

View File

@ -113,7 +113,7 @@ public class AuthRequest extends HttpFormClient.Request {
public AuthRequest fromContext(Context context) {
build(Utils.getBuild(context));
locale(Utils.getLocale(context));
androidIdHex = Long.toHexString(LastCheckinInfo.read(context).androidId);
androidIdHex = Long.toHexString(LastCheckinInfo.read(context).getAndroidId());
return this;
}

View File

@ -21,7 +21,6 @@ import android.accounts.AccountManager;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
@ -36,7 +35,6 @@ import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.webkit.CookieManager;
import android.webkit.JavascriptInterface;
import android.webkit.ValueCallback;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
@ -72,6 +70,7 @@ import static android.view.KeyEvent.KEYCODE_BACK;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT;
import static org.microg.gms.auth.AuthPrefs.isAuthVisible;
import static org.microg.gms.common.Constants.GMS_PACKAGE_NAME;
import static org.microg.gms.common.Constants.GMS_VERSION_CODE;
@ -136,7 +135,7 @@ public class LoginActivity extends AssistantActivity {
AccountManager accountManager = AccountManager.get(this);
Account account = new Account(getIntent().getStringExtra(EXTRA_EMAIL), accountType);
accountManager.addAccountExplicitly(account, getIntent().getStringExtra(EXTRA_TOKEN), null);
if (AuthManager.isAuthVisible(this) && SDK_INT >= Build.VERSION_CODES.O) {
if (isAuthVisible(this) && SDK_INT >= Build.VERSION_CODES.O) {
accountManager.setAccountVisibility(account, PACKAGE_NAME_KEY_LEGACY_NOT_VISIBLE, VISIBILITY_USER_MANAGED_VISIBLE);
}
retrieveGmsToken(account);
@ -223,7 +222,7 @@ public class LoginActivity extends AssistantActivity {
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = cm.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isConnected()) {
if (LastCheckinInfo.read(this).androidId == 0) {
if (LastCheckinInfo.read(this).getAndroidId() == 0) {
new Thread(() -> {
Runnable next;
next = checkin(false) ? this::loadLoginPage : () -> showError(R.string.auth_general_error_desc);
@ -425,7 +424,7 @@ public class LoginActivity extends AssistantActivity {
@JavascriptInterface
public final String getAndroidId() {
long androidId = LastCheckinInfo.read(LoginActivity.this).androidId;
long androidId = LastCheckinInfo.read(LoginActivity.this).getAndroidId();
Log.d(TAG, "JSBridge: getAndroidId " + androidId);
if (androidId == 0 || androidId == -1) return null;
return Long.toHexString(androidId);

View File

@ -18,15 +18,12 @@ package org.microg.gms.checkin;
import android.util.Log;
import com.squareup.wire.Wire;
import org.microg.gms.common.Build;
import org.microg.gms.common.DeviceConfiguration;
import org.microg.gms.common.DeviceIdentifier;
import org.microg.gms.common.PhoneInfo;
import org.microg.gms.common.Utils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@ -46,8 +43,8 @@ import java.util.zip.GZIPOutputStream;
public class CheckinClient {
private static final String TAG = "GmsCheckinClient";
private static final Object TODO = null; // TODO
private static final List<String> TODO_LIST_STRING = new ArrayList<String>(); // TODO
private static final List<CheckinRequest.Checkin.Statistic> TODO_LIST_CHECKIN = new ArrayList<CheckinRequest.Checkin.Statistic>(); // TODO
private static final List<String> TODO_LIST_STRING = new ArrayList<>(); // TODO
private static final List<CheckinRequest.Checkin.Statistic> TODO_LIST_CHECKIN = new ArrayList<>(); // TODO
private static final String SERVICE_URL = "https://android.clients.google.com/checkin";
public static CheckinResponse request(CheckinRequest request) throws IOException {
@ -84,8 +81,8 @@ public class CheckinClient {
LastCheckinInfo checkinInfo, Locale locale,
List<Account> accounts) {
CheckinRequest.Builder builder = new CheckinRequest.Builder()
.accountCookie(new ArrayList<String>())
.androidId(checkinInfo.androidId)
.accountCookie(new ArrayList<>())
.androidId(checkinInfo.getAndroidId())
.checkin(new CheckinRequest.Checkin.Builder()
.build(new CheckinRequest.Checkin.Build.Builder()
.bootloader(build.bootloader)
@ -105,11 +102,11 @@ public class CheckinClient {
.build())
.cellOperator(phoneInfo.cellOperator)
.event(Collections.singletonList(new CheckinRequest.Checkin.Event.Builder()
.tag(checkinInfo.androidId == 0 ? "event_log_start" : "system_update")
.value(checkinInfo.androidId == 0 ? null : "1536,0,-1,NULL")
.tag(checkinInfo.getAndroidId() == 0 ? "event_log_start" : "system_update")
.value(checkinInfo.getAndroidId() == 0 ? null : "1536,0,-1,NULL")
.timeMs(new Date().getTime())
.build()))
.lastCheckinMs(checkinInfo.lastCheckin)
.lastCheckinMs(checkinInfo.getLastCheckin())
.requestedGroup(TODO_LIST_STRING)
.roaming(phoneInfo.roaming)
.simOperator(phoneInfo.simOperator)
@ -133,7 +130,7 @@ public class CheckinClient {
.touchScreen(deviceConfiguration.touchScreen)
.widthPixels(deviceConfiguration.widthPixels)
.build())
.digest(checkinInfo.digest)
.digest(checkinInfo.getDigest())
.esn(deviceIdent.esn)
.fragment(0)
.locale(locale.toString())
@ -154,8 +151,8 @@ public class CheckinClient {
builder.macAddress(Arrays.asList(deviceIdent.wifiMac))
.macAddressType(Arrays.asList("wifi"));
}
if (checkinInfo.securityToken != 0) {
builder.securityToken(checkinInfo.securityToken)
if (checkinInfo.getSecurityToken() != 0) {
builder.securityToken(checkinInfo.getSecurityToken())
.fragment(1);
}
return builder.build();

View File

@ -39,9 +39,9 @@ public class CheckinManager {
@SuppressWarnings("MissingPermission")
public static synchronized LastCheckinInfo checkin(Context context, boolean force) throws IOException {
LastCheckinInfo info = LastCheckinInfo.read(context);
if (!force && info.lastCheckin > System.currentTimeMillis() - MIN_CHECKIN_INTERVAL)
if (!force && info.getLastCheckin() > System.currentTimeMillis() - MIN_CHECKIN_INTERVAL)
return null;
if (!CheckinPrefs.get(context).isEnabled())
if (!CheckinPrefs.isEnabled(context))
return null;
List<CheckinClient.Account> accounts = new ArrayList<CheckinClient.Account>();
AccountManager accountManager = AccountManager.get(context);
@ -63,13 +63,7 @@ public class CheckinManager {
}
private static LastCheckinInfo handleResponse(Context context, CheckinResponse response) {
LastCheckinInfo info = new LastCheckinInfo();
info.androidId = response.androidId;
info.lastCheckin = response.timeMs;
info.securityToken = response.securityToken;
info.digest = response.digest;
info.versionInfo = response.versionInfo;
info.deviceDataVersionInfo = response.deviceDataVersionInfo;
LastCheckinInfo info = new LastCheckinInfo(response);
info.write(context);
ContentResolver resolver = context.getContentResolver();

View File

@ -1,75 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.checkin;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Log;
import org.microg.gms.common.PackageUtils;
import java.io.File;
public class CheckinPrefs implements SharedPreferences.OnSharedPreferenceChangeListener {
public static final String PREF_ENABLE_CHECKIN = "checkin_enable_service";
private static CheckinPrefs INSTANCE;
public static CheckinPrefs get(Context context) {
if (INSTANCE == null) {
PackageUtils.warnIfNotMainProcess(context, CheckinPrefs.class);
if (context == null) return new CheckinPrefs(null);
INSTANCE = new CheckinPrefs(context.getApplicationContext());
}
return INSTANCE;
}
private SharedPreferences preferences;
private SharedPreferences systemDefaultPreferences;
private boolean checkinEnabled = false;
private CheckinPrefs(Context context) {
if (context != null) {
preferences = PreferenceManager.getDefaultSharedPreferences(context);
preferences.registerOnSharedPreferenceChangeListener(this);
try {
systemDefaultPreferences = (SharedPreferences) Context.class.getDeclaredMethod("getSharedPreferences", File.class, int.class).invoke(context, new File("/system/etc/microg.xml"), Context.MODE_PRIVATE);
} catch (Exception ignored) {
}
update();
}
}
private boolean getSettingsBoolean(String key, boolean def) {
if (systemDefaultPreferences != null) {
def = systemDefaultPreferences.getBoolean(key, def);
}
return preferences.getBoolean(key, def);
}
private void update() {
checkinEnabled = getSettingsBoolean(PREF_ENABLE_CHECKIN, false);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
update();
}
public boolean isEnabled() {
return checkinEnabled;
}
public static void setEnabled(Context context, boolean newStatus) {
boolean changed = CheckinPrefs.get(context).isEnabled() != newStatus;
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(PREF_ENABLE_CHECKIN, newStatus).commit();
if (!changed) return;
if (newStatus) {
context.sendOrderedBroadcast(new Intent(context, TriggerReceiver.class), null);
}
}
}

View File

@ -57,7 +57,7 @@ public class CheckinService extends IntentService {
private ICheckinService iface = new ICheckinService.Stub() {
@Override
public String getDeviceDataVersionInfo() throws RemoteException {
return LastCheckinInfo.read(CheckinService.this).deviceDataVersionInfo;
return LastCheckinInfo.read(CheckinService.this).getDeviceDataVersionInfo();
}
};
@ -70,10 +70,10 @@ public class CheckinService extends IntentService {
protected void onHandleIntent(Intent intent) {
try {
ForegroundServiceContext.completeForegroundService(this, intent, TAG);
if (CheckinPrefs.get(this).isEnabled()) {
if (CheckinPrefs.isEnabled(this)) {
LastCheckinInfo info = CheckinManager.checkin(this, intent.getBooleanExtra(EXTRA_FORCE_CHECKIN, false));
if (info != null) {
Log.d(TAG, "Checked in as " + Long.toHexString(info.androidId));
Log.d(TAG, "Checked in as " + Long.toHexString(info.getAndroidId()));
String accountType = AuthConstants.DEFAULT_ACCOUNT_TYPE;
for (Account account : AccountManager.get(this).getAccountsByType(accountType)) {
PeopleManager.loadUserInfo(this, account);
@ -86,7 +86,7 @@ public class CheckinService extends IntentService {
ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
if (receiver != null) {
Bundle bundle = new Bundle();
bundle.putLong(EXTRA_NEW_CHECKIN_TIME, info.lastCheckin);
bundle.putLong(EXTRA_NEW_CHECKIN_TIME, info.getLastCheckin());
receiver.send(Activity.RESULT_OK, bundle);
}
}
@ -115,6 +115,6 @@ public class CheckinService extends IntentService {
static void schedule(Context context) {
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent pendingIntent = PendingIntent.getService(context, TriggerReceiver.class.getName().hashCode(), new Intent(context, TriggerReceiver.class), PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.set(AlarmManager.RTC, Math.max(LastCheckinInfo.read(context).lastCheckin + REGULAR_CHECKIN_INTERVAL, System.currentTimeMillis() + BACKUP_CHECKIN_DELAY), pendingIntent);
alarmManager.set(AlarmManager.RTC, Math.max(LastCheckinInfo.read(context).getLastCheckin() + REGULAR_CHECKIN_INTERVAL, System.currentTimeMillis() + BACKUP_CHECKIN_DELAY), pendingIntent);
}
}

View File

@ -1,60 +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.checkin;
import android.content.Context;
import android.content.SharedPreferences;
public class LastCheckinInfo {
public static final String PREFERENCES_NAME = "checkin";
public static final String PREF_ANDROID_ID = "androidId";
public static final String PREF_DIGEST = "digest";
public static final String PREF_LAST_CHECKIN = "lastCheckin";
public static final String PREF_SECURITY_TOKEN = "securityToken";
public static final String PREF_VERSION_INFO = "versionInfo";
public static final String PREF_DEVICE_DATA_VERSION_INFO = "deviceDataVersionInfo";
public static final String INITIAL_DIGEST = "1-929a0dca0eee55513280171a8585da7dcd3700f8";
public long lastCheckin;
public long androidId;
public long securityToken;
public String digest;
public String versionInfo;
public String deviceDataVersionInfo;
public static LastCheckinInfo read(Context context) {
LastCheckinInfo info = new LastCheckinInfo();
SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
info.androidId = preferences.getLong(PREF_ANDROID_ID, 0);
info.digest = preferences.getString(PREF_DIGEST, INITIAL_DIGEST);
info.lastCheckin = preferences.getLong(PREF_LAST_CHECKIN, 0);
info.securityToken = preferences.getLong(PREF_SECURITY_TOKEN, 0);
info.versionInfo = preferences.getString(PREF_VERSION_INFO, "");
info.deviceDataVersionInfo = preferences.getString(PREF_DEVICE_DATA_VERSION_INFO, "");
return info;
}
public void write(Context context) {
context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE).edit()
.putLong(PREF_ANDROID_ID, androidId)
.putString(PREF_DIGEST, digest)
.putLong(PREF_LAST_CHECKIN, lastCheckin)
.putLong(PREF_SECURITY_TOKEN, securityToken)
.putString(PREF_VERSION_INFO, versionInfo)
.putString(PREF_DEVICE_DATA_VERSION_INFO, deviceDataVersionInfo)
.commit();
}
}

View File

@ -38,8 +38,8 @@ public class TriggerReceiver extends WakefulBroadcastReceiver {
try {
boolean force = "android.provider.Telephony.SECRET_CODE".equals(intent.getAction());
if (CheckinPrefs.get(context).isEnabled() || force) {
if (LastCheckinInfo.read(context).lastCheckin > System.currentTimeMillis() - REGULAR_CHECKIN_INTERVAL && !force) {
if (CheckinPrefs.isEnabled(context) || force) {
if (LastCheckinInfo.read(context).getLastCheckin() > System.currentTimeMillis() - REGULAR_CHECKIN_INTERVAL && !force) {
CheckinService.schedule(context);
return;
}

View File

@ -1,305 +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.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.preference.PreferenceManager;
import android.util.Log;
import org.microg.gms.common.PackageUtils;
import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class GcmPrefs implements SharedPreferences.OnSharedPreferenceChangeListener {
public static final String PREF_FULL_LOG = "gcm_full_log";
public static final String PREF_LAST_PERSISTENT_ID = "gcm_last_persistent_id";
public static final String PREF_CONFIRM_NEW_APPS = "gcm_confirm_new_apps";
public static final String PREF_ENABLE_GCM = "gcm_enable_mcs_service";
public static final String PREF_NETWORK_MOBILE = "gcm_network_mobile";
public static final String PREF_NETWORK_WIFI = "gcm_network_wifi";
public static final String PREF_NETWORK_ROAMING = "gcm_network_roaming";
public static final String PREF_NETWORK_OTHER = "gcm_network_other";
public static final String PREF_LEARNT_MOBILE = "gcm_learnt_mobile";
public static final String PREF_LEARNT_WIFI = "gcm_learnt_wifi";
public static final String PREF_LEARNT_OTHER = "gcm_learnt_other";
private static final int MIN_INTERVAL = 5 * 60 * 1000; // 5 minutes
private static final int MAX_INTERVAL = 30 * 60 * 1000; // 30 minutes
private static GcmPrefs INSTANCE;
public static GcmPrefs get(Context context) {
if (INSTANCE == null) {
PackageUtils.warnIfNotPersistentProcess(GcmPrefs.class);
INSTANCE = new GcmPrefs(context.getApplicationContext());
}
return INSTANCE;
}
private boolean gcmLogEnabled = true;
private String lastPersistedId = "";
private boolean confirmNewApps = false;
private boolean gcmEnabled = false;
private int networkMobile = 0;
private int networkWifi = 0;
private int networkRoaming = 0;
private int networkOther = 0;
private int learntWifi = 300000;
private int learntMobile = 300000;
private int learntOther = 300000;
private final Context context;
private SharedPreferences preferences;
private SharedPreferences systemDefaultPreferences;
private GcmPrefs(Context context) {
this.context = context;
preferences = PreferenceManager.getDefaultSharedPreferences(context);
preferences.registerOnSharedPreferenceChangeListener(this);
try {
systemDefaultPreferences = (SharedPreferences) Context.class.getDeclaredMethod("getSharedPreferences", File.class, int.class).invoke(context, new File("/system/etc/microg.xml"), Context.MODE_PRIVATE);
} catch (Exception ignored) {
}
update();
}
private boolean getSettingsBoolean(String key, boolean def) {
if (systemDefaultPreferences != null) {
def = systemDefaultPreferences.getBoolean(key, def);
}
return preferences.getBoolean(key, def);
}
public void update() {
gcmEnabled = getSettingsBoolean(PREF_ENABLE_GCM, false);
gcmLogEnabled = getSettingsBoolean(PREF_FULL_LOG, true);
confirmNewApps = getSettingsBoolean(PREF_CONFIRM_NEW_APPS, false);
lastPersistedId = preferences.getString(PREF_LAST_PERSISTENT_ID, "");
networkMobile = Integer.parseInt(preferences.getString(PREF_NETWORK_MOBILE, "0"));
networkWifi = Integer.parseInt(preferences.getString(PREF_NETWORK_WIFI, "0"));
networkRoaming = Integer.parseInt(preferences.getString(PREF_NETWORK_ROAMING, "0"));
networkOther = Integer.parseInt(preferences.getString(PREF_NETWORK_OTHER, "0"));
learntMobile = preferences.getInt(PREF_LEARNT_MOBILE, 300000);
learntWifi = preferences.getInt(PREF_LEARNT_WIFI, 300000);
learntOther = preferences.getInt(PREF_LEARNT_OTHER, 300000);
}
public String getNetworkPrefForInfo(NetworkInfo info) {
if (info == null) return PREF_NETWORK_OTHER;
if (info.isRoaming()) return PREF_NETWORK_ROAMING;
switch (info.getType()) {
case ConnectivityManager.TYPE_MOBILE:
return PREF_NETWORK_MOBILE;
case ConnectivityManager.TYPE_WIFI:
return PREF_NETWORK_WIFI;
default:
return PREF_NETWORK_OTHER;
}
}
public int getHeartbeatMsFor(NetworkInfo info) {
return getHeartbeatMsFor(getNetworkPrefForInfo(info));
}
public int getMobileInterval() {
return networkMobile;
}
public int getWifiInterval() {
return networkWifi;
}
public int getRoamingInterval() {
return networkRoaming;
}
public int getOtherInterval() {
return networkOther;
}
public void setMobileInterval(int value) {
this.networkMobile = value;
preferences.edit().putString(PREF_NETWORK_MOBILE, Integer.toString(networkMobile)).apply();
}
public void setWifiInterval(int value) {
this.networkWifi = value;
preferences.edit().putString(PREF_NETWORK_WIFI, Integer.toString(networkWifi)).apply();
}
public void setRoamingInterval(int value) {
this.networkRoaming = value;
preferences.edit().putString(PREF_NETWORK_ROAMING, Integer.toString(networkRoaming)).apply();
}
public void setOtherInterval(int value) {
this.networkOther = value;
preferences.edit().putString(PREF_NETWORK_OTHER, Integer.toString(networkOther)).apply();
}
public int getHeartbeatMsFor(String pref) {
if (PREF_NETWORK_ROAMING.equals(pref)) {
if (networkRoaming != 0) return networkRoaming * 60000;
else return learntMobile;
} else if (PREF_NETWORK_MOBILE.equals(pref)) {
if (networkMobile != 0) return networkMobile * 60000;
else return learntMobile;
} else if (PREF_NETWORK_WIFI.equals(pref)) {
if (networkWifi != 0) return networkWifi * 60000;
else return learntWifi;
} else {
if (networkOther != 0) return networkOther * 60000;
else return learntOther;
}
}
public int getNetworkValue(String pref) {
switch (pref) {
case PREF_NETWORK_MOBILE:
return networkMobile;
case PREF_NETWORK_ROAMING:
return networkRoaming;
case PREF_NETWORK_WIFI:
return networkWifi;
default:
return networkOther;
}
}
public void learnTimeout(String pref) {
Log.d("GmsGcmPrefs", "learnTimeout: " + pref);
switch (pref) {
case PREF_NETWORK_MOBILE:
case PREF_NETWORK_ROAMING:
learntMobile *= 0.95;
break;
case PREF_NETWORK_WIFI:
learntWifi *= 0.95;
break;
default:
learntOther *= 0.95;
break;
}
updateLearntValues();
}
public void learnReached(String pref, long time) {
Log.d("GmsGcmPrefs", "learnReached: " + pref + " / " + time);
switch (pref) {
case PREF_NETWORK_MOBILE:
case PREF_NETWORK_ROAMING:
if (time > learntMobile / 4 * 3)
learntMobile *= 1.02;
break;
case PREF_NETWORK_WIFI:
if (time > learntWifi / 4 * 3)
learntWifi *= 1.02;
break;
default:
if (time > learntOther / 4 * 3)
learntOther *= 1.02;
break;
}
updateLearntValues();
}
private void updateLearntValues() {
learntMobile = Math.max(MIN_INTERVAL, Math.min(learntMobile, MAX_INTERVAL));
learntWifi = Math.max(MIN_INTERVAL, Math.min(learntWifi, MAX_INTERVAL));
learntOther = Math.max(MIN_INTERVAL, Math.min(learntOther, MAX_INTERVAL));
preferences.edit().putInt(PREF_LEARNT_MOBILE, learntMobile).putInt(PREF_LEARNT_WIFI, learntWifi).putInt(PREF_LEARNT_OTHER, learntOther).apply();
}
public int getLearntMobileInterval() {
return learntMobile;
}
public int getLearntWifiInterval() {
return learntWifi;
}
public int getLearntOtherInterval() {
return learntOther;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
update();
}
public boolean isEnabled() {
return gcmEnabled;
}
public void setEnabled(boolean value) {
boolean changed = gcmEnabled != value;
preferences.edit().putBoolean(GcmPrefs.PREF_ENABLE_GCM, value).apply();
if (!changed) return;
if (!value) {
McsService.stop(context);
} else {
context.sendBroadcast(new Intent(TriggerReceiver.FORCE_TRY_RECONNECT, null, context, TriggerReceiver.class));
}
}
public boolean isEnabledFor(NetworkInfo info) {
return isEnabled() && info != null && getHeartbeatMsFor(info) >= 0;
}
public boolean isGcmLogEnabled() {
return gcmLogEnabled;
}
public boolean isConfirmNewApps() {
return confirmNewApps;
}
public void setConfirmNewApps(boolean value) {
confirmNewApps = value;
preferences.edit().putBoolean(PREF_CONFIRM_NEW_APPS, value).apply();
}
public List<String> getLastPersistedIds() {
if (lastPersistedId.isEmpty()) return Collections.emptyList();
return Arrays.asList(lastPersistedId.split("\\|"));
}
public void extendLastPersistedId(String id) {
if (!lastPersistedId.isEmpty()) lastPersistedId += "|";
lastPersistedId += id;
preferences.edit().putString(PREF_LAST_PERSISTENT_ID, lastPersistedId).apply();
}
public void clearLastPersistedId() {
lastPersistedId = "";
preferences.edit().putString(PREF_LAST_PERSISTENT_ID, lastPersistedId).apply();
}
}

View File

@ -73,6 +73,7 @@ import okio.ByteString;
import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
import static android.os.Build.VERSION.SDK_INT;
import static org.microg.gms.common.PackageUtils.warnIfNotPersistentProcess;
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_OVERRIDE;
@ -234,6 +235,7 @@ public class McsService extends Service implements Handler.Callback {
}
public synchronized static boolean isConnected(Context context) {
warnIfNotPersistentProcess(McsService.class);
if (inputStream == null || !inputStream.isAlive() || outputStream == null || !outputStream.isAlive()) {
logd(null, "Connection is not enabled or dead.");
return false;
@ -244,13 +246,14 @@ public class McsService extends Service implements Handler.Callback {
closeAll();
} else if (SystemClock.elapsedRealtime() - lastHeartbeatAckElapsedRealtime > 2 * heartbeatMs) {
logd(null, "No heartbeat for " + (SystemClock.elapsedRealtime() - lastHeartbeatAckElapsedRealtime) / 1000 + " seconds, connection assumed to be dead after " + 2 * heartbeatMs / 1000 + " seconds");
GcmPrefs.get(context).learnTimeout(activeNetworkPref);
GcmPrefs.get(context).learnTimeout(context, activeNetworkPref);
return false;
}
return true;
}
public static long getStartTimestamp() {
warnIfNotPersistentProcess(McsService.class);
return startTimestamp;
}
@ -460,7 +463,7 @@ public class McsService extends Service implements Handler.Callback {
private void handleLoginResponse(LoginResponse loginResponse) {
if (loginResponse.error == null) {
GcmPrefs.get(this).clearLastPersistedId();
GcmPrefs.clearLastPersistedId(this);
logd(this, "Logged in");
wakeLock.release();
} else {
@ -470,7 +473,7 @@ public class McsService extends Service implements Handler.Callback {
private void handleCloudMessage(DataMessageStanza message) {
if (message.persistent_id != null) {
GcmPrefs.get(this).extendLastPersistedId(message.persistent_id);
GcmPrefs.get(this).extendLastPersistedId(this, message.persistent_id);
}
if (SELF_CATEGORY.equals(message.category)) {
handleSelfMessage(message);
@ -488,7 +491,7 @@ public class McsService extends Service implements Handler.Callback {
}
private void handleHeartbeatAck(HeartbeatAck ack) {
GcmPrefs.get(this).learnReached(activeNetworkPref, SystemClock.elapsedRealtime() - lastIncomingNetworkRealtime);
GcmPrefs.get(this).learnReached(this, activeNetworkPref, SystemClock.elapsedRealtime() - lastIncomingNetworkRealtime);
lastHeartbeatAckElapsedRealtime = SystemClock.elapsedRealtime();
wakeLock.release();
}
@ -498,13 +501,13 @@ public class McsService extends Service implements Handler.Callback {
return new LoginRequest.Builder()
.adaptive_heartbeat(false)
.auth_service(LoginRequest.AuthService.ANDROID_ID)
.auth_token(Long.toString(info.securityToken))
.auth_token(Long.toString(info.getSecurityToken()))
.id("android-" + SDK_INT)
.domain("mcs.android.com")
.device_id("android-" + Long.toHexString(info.androidId))
.device_id("android-" + Long.toHexString(info.getAndroidId()))
.network_type(1)
.resource(Long.toString(info.androidId))
.user(Long.toString(info.androidId))
.resource(Long.toString(info.getAndroidId()))
.user(Long.toString(info.getAndroidId()))
.use_rmq2(true)
.setting(Collections.singletonList(new Setting.Builder().name("new_vc").value("1").build()))
.received_persistent_id(GcmPrefs.get(this).getLastPersistedIds())

View File

@ -87,8 +87,8 @@ public class PushRegisterManager {
if (!request.delete) {
if (!prefs.isEnabled() ||
(app != null && !app.allowRegister) ||
LastCheckinInfo.read(context).lastCheckin <= 0 ||
(app == null && prefs.isConfirmNewApps())) {
LastCheckinInfo.read(context).getLastCheckin() <= 0 ||
(app == null && prefs.getConfirmNewApps())) {
Bundle bundle = new Bundle();
bundle.putString(EXTRA_ERROR, ERROR_SERVICE_NOT_AVAILABLE);
callback.onResult(bundle);

View File

@ -70,8 +70,8 @@ public class RegisterRequest extends HttpFormClient.Request {
}
public RegisterRequest checkin(LastCheckinInfo lastCheckinInfo) {
androidId = lastCheckinInfo.androidId;
securityToken = lastCheckinInfo.securityToken;
androidId = lastCheckinInfo.getAndroidId();
securityToken = lastCheckinInfo.getSecurityToken();
return this;
}

View File

@ -65,7 +65,7 @@ public class TriggerReceiver extends WakefulBroadcastReceiver {
McsService.resetCurrentDelay();
}
if (LastCheckinInfo.read(context).androidId == 0) {
if (LastCheckinInfo.read(context).getAndroidId() == 0) {
Log.d(TAG, "Ignoring " + intent + ": need to checkin first.");
return;
}

View File

@ -48,7 +48,7 @@ public class MessageHandler extends ServerMessageListener {
private String peerNodeId;
public MessageHandler(WearableImpl wearable, ConnectionConfiguration config) {
this(wearable, config, new Build().model, config.nodeId, LastCheckinInfo.read(wearable.getContext()).androidId);
this(wearable, config, new Build().model, config.nodeId, LastCheckinInfo.read(wearable.getContext()).getAndroidId());
}
private MessageHandler(WearableImpl wearable, ConnectionConfiguration config, String name, String networkId, long androidId) {

View File

@ -0,0 +1,23 @@
package org.microg.gms.auth
import android.content.Context
import org.microg.gms.settings.SettingsContract
import org.microg.gms.settings.SettingsContract.Auth
object AuthPrefs {
@JvmStatic
fun isTrustGooglePermitted(context: Context): Boolean {
return SettingsContract.getSettings(context, Auth.CONTENT_URI, arrayOf(Auth.TRUST_GOOGLE)) { c ->
c.getInt(0) != 0
}
}
@JvmStatic
fun isAuthVisible(context: Context): Boolean {
return SettingsContract.getSettings(context, Auth.CONTENT_URI, arrayOf(Auth.VISIBLE)) { c ->
c.getInt(0) != 0
}
}
}

View File

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.checkin
import android.content.Context
import org.microg.gms.settings.SettingsContract
import org.microg.gms.settings.SettingsContract.CheckIn
object CheckinPrefs {
@JvmStatic
fun isEnabled(context: Context): Boolean {
val projection = arrayOf(CheckIn.ENABLED)
return SettingsContract.getSettings(context, CheckIn.CONTENT_URI, projection) { c ->
c.getInt(0) != 0
}
}
}

View File

@ -0,0 +1,82 @@
/*
* 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.checkin
import android.content.Context
import org.microg.gms.settings.SettingsContract
import org.microg.gms.settings.SettingsContract.CheckIn
data class LastCheckinInfo(
val lastCheckin: Long,
val androidId: Long,
val securityToken: Long,
val digest: String,
val versionInfo: String,
val deviceDataVersionInfo: String,
) {
constructor(r: CheckinResponse) : this(
lastCheckin = r.timeMs ?: 0L,
androidId = r.androidId ?: 0L,
securityToken = r.securityToken ?: 0L,
digest = r.digest ?: CheckIn.INITIAL_DIGEST,
versionInfo = r.versionInfo ?: "",
deviceDataVersionInfo = r.deviceDataVersionInfo ?: "",
)
companion object {
@JvmStatic
fun read(context: Context): LastCheckinInfo {
val projection = arrayOf(
CheckIn.ANDROID_ID,
CheckIn.DIGEST,
CheckIn.LAST_CHECK_IN,
CheckIn.SECURITY_TOKEN,
CheckIn.VERSION_INFO,
CheckIn.DEVICE_DATA_VERSION_INFO,
)
return SettingsContract.getSettings(context, CheckIn.CONTENT_URI, projection) { c ->
LastCheckinInfo(
androidId = c.getLong(0),
digest = c.getString(1),
lastCheckin = c.getLong(2),
securityToken = c.getLong(3),
versionInfo = c.getString(4),
deviceDataVersionInfo = c.getString(5),
)
}
}
@JvmStatic
fun clear(context: Context) = SettingsContract.setSettings(context, CheckIn.CONTENT_URI) {
put(CheckIn.ANDROID_ID, 0L)
put(CheckIn.DIGEST, CheckIn.INITIAL_DIGEST)
put(CheckIn.LAST_CHECK_IN, 0L)
put(CheckIn.SECURITY_TOKEN, 0L)
put(CheckIn.VERSION_INFO, "")
put(CheckIn.DEVICE_DATA_VERSION_INFO, "")
}
}
fun write(context: Context) = SettingsContract.setSettings(context, CheckIn.CONTENT_URI) {
put(CheckIn.ANDROID_ID, androidId)
put(CheckIn.DIGEST, digest)
put(CheckIn.LAST_CHECK_IN, lastCheckin)
put(CheckIn.SECURITY_TOKEN, securityToken)
put(CheckIn.VERSION_INFO, versionInfo)
put(CheckIn.DEVICE_DATA_VERSION_INFO, deviceDataVersionInfo)
}
}

View File

@ -5,89 +5,38 @@
package org.microg.gms.checkin
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.microg.gms.settings.SettingsContract.CheckIn
import org.microg.gms.settings.SettingsContract.getSettings
import org.microg.gms.settings.SettingsContract.setSettings
import java.io.Serializable
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
private const val ACTION_SERVICE_INFO_REQUEST = "org.microg.gms.checkin.SERVICE_INFO_REQUEST"
private const val ACTION_UPDATE_CONFIGURATION = "org.microg.gms.checkin.UPDATE_CONFIGURATION"
private const val ACTION_SERVICE_INFO_RESPONSE = "org.microg.gms.checkin.SERVICE_INFO_RESPONSE"
private const val EXTRA_SERVICE_INFO = "org.microg.gms.checkin.SERVICE_INFO"
private const val EXTRA_CONFIGURATION = "org.microg.gms.checkin.CONFIGURATION"
private const val TAG = "GmsCheckinStatusInfo"
data class ServiceInfo(val configuration: ServiceConfiguration, val lastCheckin: Long, val androidId: Long) : Serializable
data class ServiceConfiguration(val enabled: Boolean) : Serializable {
fun saveToPrefs(context: Context) {
CheckinPrefs.setEnabled(context, enabled)
data class ServiceConfiguration(val enabled: Boolean) : Serializable
suspend fun getCheckinServiceInfo(context: Context): ServiceInfo = withContext(Dispatchers.IO) {
val projection = arrayOf(CheckIn.ENABLED, CheckIn.LAST_CHECK_IN, CheckIn.ANDROID_ID)
getSettings(context, CheckIn.CONTENT_URI, projection) { c ->
ServiceInfo(
configuration = ServiceConfiguration(c.getInt(0) != 0),
lastCheckin = c.getLong(1),
androidId = c.getLong(2),
)
}
}
private fun CheckinPrefs.toConfiguration(): ServiceConfiguration = ServiceConfiguration(isEnabled)
class ServiceInfoReceiver : BroadcastReceiver() {
private fun sendInfoResponse(context: Context) {
context.sendOrderedBroadcast(Intent(ACTION_SERVICE_INFO_RESPONSE).apply {
setPackage(context.packageName)
val checkinInfo = LastCheckinInfo.read(context)
putExtra(EXTRA_SERVICE_INFO, ServiceInfo(CheckinPrefs.get(context).toConfiguration(), checkinInfo.lastCheckin, checkinInfo.androidId))
}, null)
suspend fun setCheckinServiceConfiguration(context: Context, configuration: ServiceConfiguration) = withContext(Dispatchers.IO) {
val serviceInfo = getCheckinServiceInfo(context)
if (serviceInfo.configuration == configuration) return@withContext
// enabled state is not already set, setting it now
setSettings(context, CheckIn.CONTENT_URI) {
put(CheckIn.ENABLED, configuration.enabled)
}
override fun onReceive(context: Context, intent: Intent) {
try {
when (intent.action) {
ACTION_UPDATE_CONFIGURATION -> {
(intent.getSerializableExtra(EXTRA_CONFIGURATION) as? ServiceConfiguration)?.saveToPrefs(context)
}
}
sendInfoResponse(context)
} catch (e: Exception) {
Log.w(TAG, e)
}
if (configuration.enabled) {
context.sendOrderedBroadcast(Intent(context, TriggerReceiver::class.java), null)
}
}
private suspend fun sendToServiceInfoReceiver(intent: Intent, context: Context): ServiceInfo = suspendCoroutine {
context.registerReceiver(object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
context.unregisterReceiver(this)
val serviceInfo = try {
intent.getSerializableExtra(EXTRA_SERVICE_INFO) as ServiceInfo
} catch (e: Exception) {
it.resumeWithException(e)
return
}
try {
it.resume(serviceInfo)
} catch (e: Exception) {
Log.w(TAG, e)
}
}
}, IntentFilter(ACTION_SERVICE_INFO_RESPONSE))
try {
context.sendOrderedBroadcast(intent, null)
} catch (e: Exception) {
it.resumeWithException(e)
}
}
suspend fun getCheckinServiceInfo(context: Context): ServiceInfo = sendToServiceInfoReceiver(
Intent(context, ServiceInfoReceiver::class.java).apply {
action = ACTION_SERVICE_INFO_REQUEST
}, context)
suspend fun setCheckinServiceConfiguration(context: Context, configuration: ServiceConfiguration): ServiceInfo = sendToServiceInfoReceiver(
Intent(context, ServiceInfoReceiver::class.java).apply {
action = ACTION_UPDATE_CONFIGURATION
putExtra(EXTRA_CONFIGURATION, configuration)
}, context)

View File

@ -0,0 +1,181 @@
package org.microg.gms.gcm
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkInfo
import android.util.Log
import org.microg.gms.gcm.TriggerReceiver.FORCE_TRY_RECONNECT
import org.microg.gms.settings.SettingsContract
import org.microg.gms.settings.SettingsContract.Gcm
import org.microg.gms.settings.SettingsContract.setSettings
import kotlin.math.max
import kotlin.math.min
data class GcmPrefs(
val isGcmLogEnabled: Boolean,
val lastPersistedId: String?,
val confirmNewApps: Boolean,
val gcmEnabled: Boolean,
val networkMobile: Int,
val networkWifi: Int,
val networkRoaming: Int,
val networkOther: Int,
val learntMobileInterval: Int,
val learntWifiInterval: Int,
val learntOtherInterval: Int,
) {
val isEnabled: Boolean get() = gcmEnabled
val lastPersistedIds: List<String>
get() = if (lastPersistedId.isNullOrEmpty()) emptyList() else lastPersistedId.split("\\|")
companion object {
const val PREF_CONFIRM_NEW_APPS = Gcm.CONFIRM_NEW_APPS
const val PREF_NETWORK_MOBILE = Gcm.NETWORK_MOBILE
const val PREF_NETWORK_WIFI = Gcm.NETWORK_WIFI
const val PREF_NETWORK_ROAMING = Gcm.NETWORK_ROAMING
const val PREF_NETWORK_OTHER = Gcm.NETWORK_OTHER
private const val MIN_INTERVAL = 5 * 60 * 1000 // 5 minutes
private const val MAX_INTERVAL = 30 * 60 * 1000 // 30 minutes
@JvmStatic
fun get(context: Context): GcmPrefs {
return SettingsContract.getSettings(context, Gcm.CONTENT_URI, Gcm.PROJECTION) { c ->
GcmPrefs(
isGcmLogEnabled = c.getInt(0) != 0,
lastPersistedId = c.getString(1),
confirmNewApps = c.getInt(2) != 0,
gcmEnabled = c.getInt(3) != 0,
networkMobile = c.getInt(4),
networkWifi = c.getInt(5),
networkRoaming = c.getInt(6),
networkOther = c.getInt(7),
learntMobileInterval = c.getInt(8),
learntWifiInterval = c.getInt(9),
learntOtherInterval = c.getInt(10),
)
}
}
fun write(context: Context, config: ServiceConfiguration) {
val gcmPrefs = get(context)
setSettings(context, Gcm.CONTENT_URI) {
put(Gcm.ENABLE_GCM, config.enabled)
put(Gcm.CONFIRM_NEW_APPS, config.confirmNewApps)
put(Gcm.NETWORK_MOBILE, config.mobile)
put(Gcm.NETWORK_WIFI, config.wifi)
put(Gcm.NETWORK_ROAMING, config.roaming)
put(Gcm.NETWORK_OTHER, config.other)
}
gcmPrefs.setEnabled(context, config.enabled)
}
@JvmStatic
fun clearLastPersistedId(context: Context) {
setSettings(context, Gcm.CONTENT_URI) {
put(Gcm.LAST_PERSISTENT_ID, "")
}
}
}
/**
* Call this whenever the enabled state of GCM has changed.
*/
private fun setEnabled(context: Context, enabled: Boolean) {
if (gcmEnabled == enabled) return
if (enabled) {
val i = Intent(FORCE_TRY_RECONNECT, null, context, TriggerReceiver::class.java)
context.sendBroadcast(i)
} else {
McsService.stop(context)
}
}
@Suppress("DEPRECATION")
fun getNetworkPrefForInfo(info: NetworkInfo?): String {
if (info == null) return PREF_NETWORK_OTHER
return if (info.isRoaming) PREF_NETWORK_ROAMING else when (info.type) {
ConnectivityManager.TYPE_MOBILE -> PREF_NETWORK_MOBILE
ConnectivityManager.TYPE_WIFI -> PREF_NETWORK_WIFI
else -> PREF_NETWORK_OTHER
}
}
@Suppress("DEPRECATION")
fun getHeartbeatMsFor(info: NetworkInfo?): Int {
return getHeartbeatMsFor(getNetworkPrefForInfo(info))
}
fun getHeartbeatMsFor(pref: String): Int {
return if (PREF_NETWORK_ROAMING == pref) {
if (networkRoaming != 0) networkRoaming * 60000 else learntMobileInterval
} else if (PREF_NETWORK_MOBILE == pref) {
if (networkMobile != 0) networkMobile * 60000 else learntMobileInterval
} else if (PREF_NETWORK_WIFI == pref) {
if (networkWifi != 0) networkWifi * 60000 else learntWifiInterval
} else {
if (networkOther != 0) networkOther * 60000 else learntOtherInterval
}
}
fun learnTimeout(context: Context, pref: String) {
Log.d("GmsGcmPrefs", "learnTimeout: $pref")
when (pref) {
PREF_NETWORK_MOBILE, PREF_NETWORK_ROAMING -> setSettings(context, Gcm.CONTENT_URI) {
put(Gcm.LEARNT_MOBILE, (learntMobileInterval * 0.95).toInt())
}
PREF_NETWORK_WIFI -> setSettings(context, Gcm.CONTENT_URI) {
put(Gcm.LEARNT_WIFI, (learntWifiInterval * 0.95).toInt())
}
else -> setSettings(context, Gcm.CONTENT_URI) {
put(Gcm.LEARNT_OTHER, (learntOtherInterval * 0.95).toInt())
}
}
}
fun learnReached(context: Context, pref: String, time: Long) {
Log.d("GmsGcmPrefs", "learnReached: $pref / $time")
when (pref) {
PREF_NETWORK_MOBILE, PREF_NETWORK_ROAMING -> {
if (time > learntMobileInterval / 4 * 3) {
val newInterval = (learntMobileInterval * 1.02).toInt()
setSettings(context, Gcm.CONTENT_URI) {
put(Gcm.LEARNT_MOBILE, max(MIN_INTERVAL, min(newInterval, MAX_INTERVAL)))
}
}
}
PREF_NETWORK_WIFI -> {
if (time > learntWifiInterval / 4 * 3) {
val newInterval = (learntWifiInterval * 1.02).toInt()
setSettings(context, Gcm.CONTENT_URI) {
put(Gcm.LEARNT_WIFI, max(MIN_INTERVAL, min(newInterval, MAX_INTERVAL)))
}
}
}
else -> {
if (time > learntOtherInterval / 4 * 3) {
val newInterval = (learntOtherInterval * 1.02).toInt()
setSettings(context, Gcm.CONTENT_URI) {
put(Gcm.LEARNT_OTHER, max(MIN_INTERVAL, min(newInterval, MAX_INTERVAL)))
}
}
}
}
}
@Suppress("DEPRECATION")
fun isEnabledFor(info: NetworkInfo?): Boolean {
return isEnabled && info != null && getHeartbeatMsFor(info) >= 0
}
fun extendLastPersistedId(context: Context, id: String) {
val newId = if (lastPersistedId.isNullOrEmpty()) id else "$lastPersistedId|$id"
setSettings(context, Gcm.CONTENT_URI) {
put(Gcm.LAST_PERSISTENT_ID, newId)
}
}
}

View File

@ -30,7 +30,7 @@ import kotlin.coroutines.suspendCoroutine
private const val TAG = "GmsGcmRegister"
private suspend fun ensureCheckinIsUpToDate(context: Context) {
if (!CheckinPrefs.get(context).isEnabled) throw RuntimeException("Checkin disabled")
if (!CheckinPrefs.isEnabled(context)) throw RuntimeException("Checkin disabled")
val lastCheckin = LastCheckinInfo.read(context).lastCheckin
if (lastCheckin < System.currentTimeMillis() - CheckinService.MAX_VALID_CHECKIN_AGE) {
val resultData: Bundle = suspendCoroutine { continuation ->
@ -55,7 +55,7 @@ private suspend fun ensureCheckinIsUpToDate(context: Context) {
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) {
if (app == null && GcmPrefs.get(context).confirmNewApps) {
val accepted: Boolean = suspendCoroutine { continuation ->
val i = Intent(context, AskPushPermission::class.java)
i.putExtra(AskPushPermission.EXTRA_REQUESTED_PACKAGE, packageName)

View File

@ -10,52 +10,40 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.Serializable
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
private const val ACTION_SERVICE_INFO_REQUEST = "org.microg.gms.gcm.SERVICE_INFO_REQUEST"
private const val ACTION_UPDATE_CONFIGURATION = "org.microg.gms.gcm.UPDATE_CONFIGURATION"
private const val ACTION_SERVICE_INFO_RESPONSE = "org.microg.gms.gcm.SERVICE_INFO_RESPONSE"
private const val EXTRA_SERVICE_INFO = "org.microg.gms.gcm.SERVICE_INFO"
private const val EXTRA_CONFIGURATION = "org.microg.gms.gcm.CONFIGURATION"
private const val TAG = "GmsGcmStatusInfo"
data class ServiceInfo(val configuration: ServiceConfiguration, val connected: Boolean, val startTimestamp: Long, val learntMobileInterval: Int, val learntWifiInterval: Int, val learntOtherInterval: Int) : Serializable
data class ServiceConfiguration(val enabled: Boolean, val confirmNewApps: Boolean, val mobile: Int, val wifi: Int, val roaming: Int, val other: Int) : Serializable {
fun saveToPrefs(context: Context) {
GcmPrefs.get(context).apply {
isEnabled = enabled
isConfirmNewApps = confirmNewApps
mobileInterval = mobile
wifiInterval = wifi
roamingInterval = roaming
otherInterval = other
}
}
}
data class ServiceConfiguration(val enabled: Boolean, val confirmNewApps: Boolean, val mobile: Int, val wifi: Int, val roaming: Int, val other: Int) : Serializable
private fun GcmPrefs.toConfiguration(): ServiceConfiguration = ServiceConfiguration(isEnabled, isConfirmNewApps, mobileInterval, wifiInterval, roamingInterval, otherInterval)
private fun GcmPrefs.toConfiguration(): ServiceConfiguration = ServiceConfiguration(isEnabled, confirmNewApps, networkMobile, networkWifi, networkRoaming, networkOther)
class ServiceInfoReceiver : BroadcastReceiver() {
private fun sendInfoResponse(context: Context) {
context.sendOrderedBroadcast(Intent(ACTION_SERVICE_INFO_RESPONSE).apply {
setPackage(context.packageName)
val prefs = GcmPrefs.get(context)
putExtra(EXTRA_SERVICE_INFO, ServiceInfo(prefs.toConfiguration(), McsService.isConnected(context), McsService.getStartTimestamp(), prefs.learntMobileInterval, prefs.learntWifiInterval, prefs.learntOtherInterval))
}, null)
}
override fun onReceive(context: Context, intent: Intent) {
try {
when (intent.action) {
ACTION_UPDATE_CONFIGURATION -> {
(intent.getSerializableExtra(EXTRA_CONFIGURATION) as? ServiceConfiguration)?.saveToPrefs(context)
}
}
sendInfoResponse(context)
context.sendOrderedBroadcast(Intent(ACTION_SERVICE_INFO_RESPONSE).apply {
setPackage(context.packageName)
val prefs = GcmPrefs.get(context)
val info = ServiceInfo(
configuration = prefs.toConfiguration(),
connected = McsService.isConnected(context),
startTimestamp = McsService.getStartTimestamp(),
learntMobileInterval = prefs.learntMobileInterval,
learntWifiInterval = prefs.learntWifiInterval,
learntOtherInterval = prefs.learntOtherInterval
)
putExtra(EXTRA_SERVICE_INFO, info)
}, null)
} catch (e: Exception) {
Log.w(TAG, e)
}
@ -87,12 +75,11 @@ private suspend fun sendToServiceInfoReceiver(intent: Intent, context: Context):
}
suspend fun getGcmServiceInfo(context: Context): ServiceInfo = sendToServiceInfoReceiver(
// this is still using a broadcast, because it calls into McsService in the persistent process
Intent(context, ServiceInfoReceiver::class.java).apply {
action = ACTION_SERVICE_INFO_REQUEST
}, context)
suspend fun setGcmServiceConfiguration(context: Context, configuration: ServiceConfiguration): ServiceInfo = sendToServiceInfoReceiver(
Intent(context, ServiceInfoReceiver::class.java).apply {
action = ACTION_UPDATE_CONFIGURATION
putExtra(EXTRA_CONFIGURATION, configuration)
}, context)
suspend fun setGcmServiceConfiguration(context: Context, configuration: ServiceConfiguration) = withContext(Dispatchers.IO) {
GcmPrefs.write(context, configuration)
}

View File

@ -0,0 +1,256 @@
package org.microg.gms.settings
import android.content.ContentProvider
import android.content.ContentValues
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import android.database.Cursor
import android.database.MatrixCursor
import android.net.Uri
import android.util.Log
import androidx.preference.PreferenceManager
import org.microg.gms.common.PackageUtils.warnIfNotMainProcess
import org.microg.gms.settings.SettingsContract.AUTHORITY
import org.microg.gms.settings.SettingsContract.Auth
import org.microg.gms.settings.SettingsContract.CheckIn
import org.microg.gms.settings.SettingsContract.Exposure
import org.microg.gms.settings.SettingsContract.Gcm
import java.io.File
/**
* All settings access should go through this [ContentProvider],
* because it provides safe access from different processes which normal [SharedPreferences] don't.
*/
class SettingsProvider : ContentProvider() {
private val preferences: SharedPreferences by lazy {
PreferenceManager.getDefaultSharedPreferences(context)
}
private val checkInPrefs by lazy {
context!!.getSharedPreferences(CheckIn.PREFERENCES_NAME, MODE_PRIVATE)
}
private val systemDefaultPreferences: SharedPreferences? by lazy {
try {
Context::class.java.getDeclaredMethod(
"getSharedPreferences",
File::class.java,
Int::class.javaPrimitiveType
).invoke(context, File("/system/etc/microg.xml"), MODE_PRIVATE) as SharedPreferences
} catch (ignored: Exception) {
null
}
}
override fun onCreate(): Boolean {
return true
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? = when (uri) {
CheckIn.CONTENT_URI -> queryCheckIn(projection ?: CheckIn.PROJECTION)
Gcm.CONTENT_URI -> queryGcm(projection ?: Gcm.PROJECTION)
Auth.CONTENT_URI -> queryAuth(projection ?: Auth.PROJECTION)
Exposure.CONTENT_URI -> queryExposure(projection ?: Exposure.PROJECTION)
else -> null
}
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
warnIfNotMainProcess(context, this.javaClass)
if (values == null) return 0
when (uri) {
CheckIn.CONTENT_URI -> updateCheckIn(values)
Gcm.CONTENT_URI -> updateGcm(values)
Auth.CONTENT_URI -> updateAuth(values)
Exposure.CONTENT_URI -> updateExposure(values)
else -> return 0
}
return 1
}
private fun queryCheckIn(p: Array<out String>): Cursor = MatrixCursor(p).addRow(p) { key ->
when (key) {
CheckIn.ENABLED -> getSettingsBoolean(key, true)
CheckIn.ANDROID_ID -> checkInPrefs.getLong(key, 0)
CheckIn.DIGEST -> checkInPrefs.getString(key, CheckIn.INITIAL_DIGEST)
?: CheckIn.INITIAL_DIGEST
CheckIn.LAST_CHECK_IN -> checkInPrefs.getLong(key, 0)
CheckIn.SECURITY_TOKEN -> checkInPrefs.getLong(key, 0)
CheckIn.VERSION_INFO -> checkInPrefs.getString(key, "") ?: ""
CheckIn.DEVICE_DATA_VERSION_INFO -> checkInPrefs.getString(key, "") ?: ""
else -> throw IllegalArgumentException()
}
}
private fun updateCheckIn(values: ContentValues) {
if (values.size() == 0) return
if (values.size() == 1 && values.containsKey(CheckIn.ENABLED)) {
// special case: only changing enabled state
updateCheckInEnabled(values.getAsBoolean(CheckIn.ENABLED))
return
}
val editor = checkInPrefs.edit()
values.valueSet().forEach { (key, value) ->
// TODO remove log
Log.e("TEST", "check-in update: $key = $value")
if (key == CheckIn.ENABLED) {
// special case: not saved in checkInPrefs
updateCheckInEnabled(value as Boolean)
}
when (key) {
CheckIn.ANDROID_ID -> editor.putLong(key, value as Long)
CheckIn.DIGEST -> editor.putString(key, value as String?)
CheckIn.LAST_CHECK_IN -> editor.putLong(key, value as Long)
CheckIn.SECURITY_TOKEN -> editor.putLong(key, value as Long)
CheckIn.VERSION_INFO -> editor.putString(key, value as String?)
CheckIn.DEVICE_DATA_VERSION_INFO -> editor.putString(key, value as String?)
}
}
editor.apply()
}
private fun updateCheckInEnabled(enabled: Boolean) {
preferences.edit()
.putBoolean(CheckIn.ENABLED, enabled)
.apply()
}
private fun queryGcm(p: Array<out String>): Cursor = MatrixCursor(p).addRow(p) { key ->
when (key) {
Gcm.ENABLE_GCM -> getSettingsBoolean(key, false)
Gcm.FULL_LOG -> getSettingsBoolean(key, true)
Gcm.CONFIRM_NEW_APPS -> getSettingsBoolean(key, false)
Gcm.LAST_PERSISTENT_ID -> preferences.getString(key, "") ?: ""
Gcm.NETWORK_MOBILE -> Integer.parseInt(preferences.getString(key, "0") ?: "0")
Gcm.NETWORK_WIFI -> Integer.parseInt(preferences.getString(key, "0") ?: "0")
Gcm.NETWORK_ROAMING -> Integer.parseInt(preferences.getString(key, "0") ?: "0")
Gcm.NETWORK_OTHER -> Integer.parseInt(preferences.getString(key, "0") ?: "0")
Gcm.LEARNT_MOBILE -> preferences.getInt(key, 300000)
Gcm.LEARNT_WIFI -> preferences.getInt(key, 300000)
Gcm.LEARNT_OTHER -> preferences.getInt(key, 300000)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
private fun updateGcm(values: ContentValues) {
if (values.size() == 0) return
val editor = preferences.edit()
values.valueSet().forEach { (key, value) ->
// TODO remove log
Log.e("TEST", "gcm update: $key = $value")
when (key) {
Gcm.ENABLE_GCM -> editor.putBoolean(key, value as Boolean)
Gcm.FULL_LOG -> editor.putBoolean(key, value as Boolean)
Gcm.CONFIRM_NEW_APPS -> editor.putBoolean(key, value as Boolean)
Gcm.LAST_PERSISTENT_ID -> editor.putString(key, value as String?)
Gcm.NETWORK_MOBILE -> editor.putString(key, (value as Int).toString())
Gcm.NETWORK_WIFI -> editor.putString(key, (value as Int).toString())
Gcm.NETWORK_ROAMING -> editor.putString(key, (value as Int).toString())
Gcm.NETWORK_OTHER -> editor.putString(key, (value as Int).toString())
Gcm.LEARNT_MOBILE -> editor.putInt(key, value as Int)
Gcm.LEARNT_WIFI -> editor.putInt(key, value as Int)
Gcm.LEARNT_OTHER -> editor.putInt(key, value as Int)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
editor.apply()
}
private fun queryAuth(p: Array<out String>): Cursor = MatrixCursor(p).addRow(p) { key ->
when (key) {
Auth.TRUST_GOOGLE -> getSettingsBoolean(key, true)
Auth.VISIBLE -> getSettingsBoolean(key, false)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
private fun updateAuth(values: ContentValues) {
if (values.size() == 0) return
val editor = preferences.edit()
values.valueSet().forEach { (key, value) ->
// TODO remove log
Log.e("TEST", "auth update: $key = $value")
when (key) {
Auth.TRUST_GOOGLE -> editor.putBoolean(key, value as Boolean)
Auth.VISIBLE -> editor.putBoolean(key, value as Boolean)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
editor.apply()
}
private fun queryExposure(p: Array<out String>): Cursor = MatrixCursor(p).addRow(p) { key ->
when (key) {
Exposure.SCANNER_ENABLED -> getSettingsBoolean(key, false)
Exposure.LAST_CLEANUP -> preferences.getLong(key, 0L)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
private fun updateExposure(values: ContentValues) {
if (values.size() == 0) return
val editor = preferences.edit()
values.valueSet().forEach { (key, value) ->
// TODO remove log
Log.e("TEST", "exposure update: $key = $value")
when (key) {
Exposure.SCANNER_ENABLED -> editor.putBoolean(key, value as Boolean)
Exposure.LAST_CLEANUP -> editor.putLong(key, value as Long)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
editor.apply()
}
private fun MatrixCursor.addRow(
p: Array<out String>,
valueGetter: (String) -> Any
): MatrixCursor {
val row = newRow()
for (key in p) row.add(valueGetter.invoke(key).apply {
// TODO remove log
Log.e("TEST", "$key = $this")
})
return this
}
override fun getType(uri: Uri): String {
return "vnd.android.cursor.item/vnd.$AUTHORITY.${uri.path}"
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
throw UnsupportedOperationException()
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
throw UnsupportedOperationException()
}
/**
* Returns the current setting of the given [key]
* using the default value from [systemDefaultPreferences] or [def] if not available.
* @return the current setting as [Int], because [ContentProvider] does not support [Boolean].
*/
private fun getSettingsBoolean(key: String, def: Boolean): Int {
val default = systemDefaultPreferences?.getBoolean(key, def) ?: def
return if (preferences.getBoolean(key, default)) 1 else 0
}
}

View File

@ -34,11 +34,12 @@ class DeviceRegistrationFragment : Fragment(R.layout.device_registration_fragmen
lifecycleScope.launchWhenResumed {
val info = getCheckinServiceInfo(requireContext())
val newConfiguration = info.configuration.copy(enabled = newStatus)
displayServiceInfo(setCheckinServiceConfiguration(requireContext(), newConfiguration))
setCheckinServiceConfiguration(requireContext(), newConfiguration)
displayServiceInfo(info.copy(configuration = newConfiguration))
}
}
fun displayServiceInfo(serviceInfo: ServiceInfo) {
private fun displayServiceInfo(serviceInfo: ServiceInfo) {
binding.checkinEnabled = serviceInfo.configuration.enabled
}

View File

@ -33,11 +33,12 @@ class PushNotificationFragment : Fragment(R.layout.push_notification_fragment) {
lifecycleScope.launchWhenResumed {
val info = getGcmServiceInfo(requireContext())
val newConfiguration = info.configuration.copy(enabled = newStatus)
displayServiceInfo(setGcmServiceConfiguration(requireContext(), newConfiguration))
setGcmServiceConfiguration(requireContext(), newConfiguration)
displayServiceInfo(info.copy(configuration = newConfiguration))
}
}
fun displayServiceInfo(serviceInfo: ServiceInfo) {
private fun displayServiceInfo(serviceInfo: ServiceInfo) {
binding.gcmEnabled = serviceInfo.configuration.enabled
}

View File

@ -35,7 +35,8 @@ class ExposureNotificationsFragment : Fragment(R.layout.exposure_notifications_f
lifecycleScope.launchWhenResumed {
val info = getExposureNotificationsServiceInfo(requireContext())
val newConfiguration = info.configuration.copy(enabled = newStatus)
displayServiceInfo(setExposureNotificationsServiceConfiguration(requireContext(), newConfiguration))
setExposureNotificationsServiceConfiguration(requireContext(), newConfiguration)
displayServiceInfo(info.copy(configuration = newConfiguration))
}
}

View File

@ -43,10 +43,6 @@
</intent-filter>
</service>
<receiver
android:name="org.microg.gms.nearby.exposurenotification.ServiceInfoReceiver"
android:process=":persistent" />
<receiver
android:name="org.microg.gms.nearby.exposurenotification.ServiceTrigger"
android:process=":persistent">

View File

@ -7,22 +7,23 @@ package org.microg.gms.nearby.exposurenotification
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import org.microg.gms.common.PackageUtils
import org.microg.gms.settings.SettingsContract.Exposure.CONTENT_URI
import org.microg.gms.settings.SettingsContract.Exposure.LAST_CLEANUP
import org.microg.gms.settings.SettingsContract.Exposure.SCANNER_ENABLED
import org.microg.gms.settings.SettingsContract.getSettings
import org.microg.gms.settings.SettingsContract.setSettings
class ExposurePreferences(private val context: Context) {
private var preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
init {
PackageUtils.warnIfNotPersistentProcess(ExposurePreferences::class.java)
}
var enabled
get() = preferences.getBoolean(PREF_SCANNER_ENABLED, false)
get() = getSettings(context, CONTENT_URI, arrayOf(SCANNER_ENABLED)) { c ->
c.getInt(0) != 0
}
set(newStatus) {
val changed = enabled != newStatus
preferences.edit().putBoolean(PREF_SCANNER_ENABLED, newStatus).commit()
setSettings(context, CONTENT_URI) {
put(SCANNER_ENABLED, newStatus)
}
if (!changed) return
if (newStatus) {
context.sendOrderedBroadcast(Intent(context, ServiceTrigger::class.java), null)
@ -33,11 +34,11 @@ class ExposurePreferences(private val context: Context) {
}
var lastCleanup
get() = preferences.getLong(PREF_LAST_CLEANUP, 0)
set(value) = preferences.edit().putLong(PREF_LAST_CLEANUP, value).apply()
get() = getSettings(context, CONTENT_URI, arrayOf(LAST_CLEANUP)) { c ->
c.getLong(0)
}
set(value) = setSettings(context, CONTENT_URI) {
put(LAST_CLEANUP, value)
}
companion object {
private const val PREF_SCANNER_ENABLED = "exposure_scanner_enabled"
private const val PREF_LAST_CLEANUP = "exposure_last_cleanup"
}
}

View File

@ -5,21 +5,10 @@
package org.microg.gms.nearby.exposurenotification
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.Serializable
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
private const val ACTION_SERVICE_INFO_REQUEST = "org.microg.gms.nearby.exposurenotification.SERVICE_INFO_REQUEST"
private const val ACTION_UPDATE_CONFIGURATION = "org.microg.gms.nearby.exposurenotification.UPDATE_CONFIGURATION"
private const val ACTION_SERVICE_INFO_RESPONSE = "org.microg.gms.nearby.exposurenotification.SERVICE_INFO_RESPONSE"
private const val EXTRA_SERVICE_INFO = "org.microg.gms.nearby.exposurenotification.SERVICE_INFO"
private const val EXTRA_CONFIGURATION = "org.microg.gms.nearby.exposurenotification.CONFIGURATION"
data class ServiceInfo(val configuration: ServiceConfiguration) : Serializable
@ -31,59 +20,12 @@ data class ServiceConfiguration(val enabled: Boolean) : Serializable {
private fun ExposurePreferences.toConfiguration(): ServiceConfiguration = ServiceConfiguration(enabled)
class ServiceInfoReceiver : BroadcastReceiver() {
private fun sendInfoResponse(context: Context) {
context.sendOrderedBroadcast(Intent(ACTION_SERVICE_INFO_RESPONSE).apply {
setPackage(context.packageName)
putExtra(EXTRA_SERVICE_INFO, ServiceInfo(ExposurePreferences(context).toConfiguration()))
}, null)
suspend fun getExposureNotificationsServiceInfo(context: Context): ServiceInfo =
withContext(Dispatchers.IO) {
ServiceInfo(ExposurePreferences(context).toConfiguration())
}
override fun onReceive(context: Context, intent: Intent) {
try {
when (intent.action) {
ACTION_UPDATE_CONFIGURATION -> {
(intent.getSerializableExtra(EXTRA_CONFIGURATION) as? ServiceConfiguration)?.saveToPrefs(context)
}
}
sendInfoResponse(context)
} catch (e: Exception) {
Log.w(TAG, e)
}
suspend fun setExposureNotificationsServiceConfiguration(context: Context, configuration: ServiceConfiguration) =
withContext(Dispatchers.IO) {
ExposurePreferences(context).enabled = configuration.enabled
}
}
private suspend fun sendToServiceInfoReceiver(intent: Intent, context: Context): ServiceInfo = suspendCoroutine {
context.registerReceiver(object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
context.unregisterReceiver(this)
val serviceInfo = try {
intent.getSerializableExtra(EXTRA_SERVICE_INFO) as ServiceInfo
} catch (e: Exception) {
it.resumeWithException(e)
return
}
try {
it.resume(serviceInfo)
} catch (e: Exception) {
Log.w(TAG, e)
}
}
}, IntentFilter(ACTION_SERVICE_INFO_RESPONSE))
try {
context.sendOrderedBroadcast(intent, null)
} catch (e: Exception) {
it.resumeWithException(e)
}
}
suspend fun getExposureNotificationsServiceInfo(context: Context): ServiceInfo = sendToServiceInfoReceiver(
Intent(context, ServiceInfoReceiver::class.java).apply {
action = ACTION_SERVICE_INFO_REQUEST
}, context)
suspend fun setExposureNotificationsServiceConfiguration(context: Context, configuration: ServiceConfiguration): ServiceInfo = sendToServiceInfoReceiver(
Intent(context, ServiceInfoReceiver::class.java).apply {
action = ACTION_UPDATE_CONFIGURATION
putExtra(EXTRA_CONFIGURATION, configuration)
}, context)