Repo Created

This commit is contained in:
Fr4nz D13trich 2025-11-15 17:44:12 +01:00
parent eb305e2886
commit a8c22c65db
4784 changed files with 329907 additions and 2 deletions

View file

@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: 2015 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
apply plugin: 'signing'
android {
namespace "com.google.android.gms.base"
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"
aidlPackagedList "com/google/android/gms/common/data/DataHolder.aidl"
aidlPackagedList "com/google/android/gms/common/images/WebImage.aidl"
aidlPackagedList "com/google/android/gms/common/api/internal/IStatusCallback.aidl"
buildFeatures {
aidl = true
}
defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
}
apply from: '../gradle/publish-android.gradle'
description = 'microG implementation of play-services-base'
dependencies {
// Dependencies from play-services-base:18.8.0
api 'androidx.collection:collection:1.0.0'
api 'androidx.core:core:1.9.0'
api 'androidx.fragment:fragment:1.1.0'
api project(':play-services-basement')
api project(':play-services-tasks')
annotationProcessor project(':safe-parcel-processor')
}

View file

@ -0,0 +1,63 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'maven-publish'
apply plugin: 'signing'
dependencies {
api project(':play-services-basement-ktx')
implementation project(":play-services-core-proto")
implementation "androidx.annotation:annotation:$annotationVersion"
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation "androidx.core:core-ktx:$coreVersion"
implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion"
implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion"
implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion"
implementation "androidx.preference:preference-ktx:$preferenceVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
}
android {
namespace "org.microg.gms.base.core"
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"
defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
}
buildFeatures {
dataBinding = true
buildConfig = true
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
kotlinOptions {
jvmTarget = 1.8
}
lintOptions {
disable 'MissingTranslation'
}
}
apply from: '../../gradle/publish-android.gradle'
description = 'microG service implementation for play-services-base'

View file

@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-FileCopyrightText: 2023 e Foundation
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
dependencies {
implementation project(':play-services-base')
implementation project(':play-services-base-core')
}
android {
namespace "org.microg.gms.core.pkg"
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"
defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
}
sourceSets {
main {
java.srcDirs = ['src/main/kotlin']
}
}
lintOptions {
disable 'MissingTranslation'
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
kotlinOptions {
jvmTarget = 1.8
}
}

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ SPDX-FileCopyrightText: 2023 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<provider
android:name="org.microg.gms.profile.ProfileProvider"
android:authorities="${applicationId}.microg.profile"
android:exported="true"
tools:ignore="ExportedContentProvider" />
<service
android:name="org.microg.gms.moduleinstall.ModuleInstallService"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.gms.chimera.container.moduleinstall.ModuleInstallService.START" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
</application>
</manifest>

View file

@ -0,0 +1,65 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.moduleinstall
import android.os.Bundle
import android.util.Log
import com.google.android.gms.common.Feature
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.Status
import com.google.android.gms.common.api.internal.IStatusCallback
import com.google.android.gms.common.internal.ConnectionInfo
import com.google.android.gms.common.internal.GetServiceRequest
import com.google.android.gms.common.internal.IGmsCallbacks
import com.google.android.gms.common.moduleinstall.ModuleAvailabilityResponse
import com.google.android.gms.common.moduleinstall.ModuleAvailabilityResponse.AvailabilityStatus.STATUS_ALREADY_AVAILABLE
import com.google.android.gms.common.moduleinstall.ModuleInstallIntentResponse
import com.google.android.gms.common.moduleinstall.ModuleInstallResponse
import com.google.android.gms.common.moduleinstall.internal.ApiFeatureRequest
import com.google.android.gms.common.moduleinstall.internal.IModuleInstallCallbacks
import com.google.android.gms.common.moduleinstall.internal.IModuleInstallService
import com.google.android.gms.common.moduleinstall.internal.IModuleInstallStatusListener
import org.microg.gms.BaseService
import org.microg.gms.common.GmsService
private const val TAG = "ModuleInstall"
class ModuleInstallService : BaseService(TAG, GmsService.MODULE_INSTALL) {
override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) {
val binder = ModuleInstallServiceImpl().asBinder()
callback.onPostInitCompleteWithConnectionInfo(CommonStatusCodes.SUCCESS, binder, ConnectionInfo().apply {
features = arrayOf(Feature("moduleinstall", 7))
})
}
}
class ModuleInstallServiceImpl : IModuleInstallService.Stub() {
override fun areModulesAvailable(callbacks: IModuleInstallCallbacks?, request: ApiFeatureRequest?) {
Log.d(TAG, "Not yet implemented: areModulesAvailable $request")
runCatching { callbacks?.onModuleAvailabilityResponse(Status.SUCCESS, ModuleAvailabilityResponse(true, STATUS_ALREADY_AVAILABLE)) }
}
override fun installModules(callbacks: IModuleInstallCallbacks?, request: ApiFeatureRequest?, listener: IModuleInstallStatusListener?) {
Log.d(TAG, "Not yet implemented: installModules $request")
runCatching { callbacks?.onModuleInstallResponse(Status.CANCELED, ModuleInstallResponse(0, true)) }
}
override fun getInstallModulesIntent(callbacks: IModuleInstallCallbacks?, request: ApiFeatureRequest?) {
Log.d(TAG, "Not yet implemented: getInstallModulesIntent $request")
runCatching { callbacks?.onModuleInstallIntentResponse(Status.CANCELED, ModuleInstallIntentResponse(null)) }
}
override fun releaseModules(callback: IStatusCallback?, request: ApiFeatureRequest?) {
Log.d(TAG, "Not yet implemented: releaseModules $request")
runCatching { callback?.onResult(Status.SUCCESS) }
}
override fun unregisterListener(callback: IStatusCallback?, listener: IModuleInstallStatusListener?) {
Log.d(TAG, "Not yet implemented: unregisterListener")
runCatching { callback?.onResult(Status.SUCCESS) }
}
}

View file

@ -0,0 +1,52 @@
/*
* SPDX-FileCopyrightText: 2023 e Foundation
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.profile
import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.database.MatrixCursor
import android.net.Uri
import org.microg.gms.settings.SettingsContract
class ProfileProvider : ContentProvider() {
val COLUMN_ID = "profile_id"
val COLUMN_VALUE = "profile_value"
override fun onCreate(): Boolean {
ProfileManager.ensureInitialized(context!!)
return true
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor =
MatrixCursor(arrayOf(COLUMN_ID, COLUMN_VALUE)).apply {
ProfileManager.getActiveProfileData(context!!).entries
.forEach {
addRow(arrayOf(it.key, it.value))
}
}
override fun getType(uri: Uri): String {
return "vnd.android.cursor.item/vnd.${SettingsContract.getAuthority(context!!)}.${uri.path}"
}
override fun insert(uri: Uri, values: ContentValues?): Nothing = throw UnsupportedOperationException()
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Nothing =
throw UnsupportedOperationException()
override fun update(
uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?
): Nothing = throw UnsupportedOperationException()
}

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<permission android:name="${applicationId}.permission.READ_SETTINGS"
android:protectionLevel="signature" />
<permission android:name="${applicationId}.permission.WRITE_SETTINGS"
android:protectionLevel="signature" />
<uses-permission android:name="android.permission.INTERACT_ACROSS_PROFILES"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"
tools:ignore="ProtectedPermissions" />
<application>
<provider
android:name="org.microg.gms.settings.SettingsProvider"
android:authorities="${applicationId}.microg.settings"
android:exported="true"
android:grantUriPermissions="true"
android:readPermission="${applicationId}.permission.READ_SETTINGS"
android:writePermission="${applicationId}.permission.WRITE_SETTINGS" />
<activity
android:name="org.microg.gms.crossprofile.CrossProfileSendActivity"
android:exported="false"
tools:targetApi="30" />
<activity
android:name="org.microg.gms.crossprofile.CrossProfileRequestActivity"
android:exported="false"
tools:targetApi="30" />
<receiver android:name="org.microg.gms.crossprofile.UserInitReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.USER_INITIALIZE" />
</intent-filter>
</receiver>
</application>
</manifest>

View file

@ -0,0 +1,276 @@
/*
* 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;
import android.accounts.Account;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import android.util.Log;
import com.google.android.gms.common.api.Scope;
import com.google.android.gms.common.internal.*;
import org.microg.gms.auth.AuthConstants;
import org.microg.gms.common.GmsService;
import java.util.EnumSet;
public abstract class AbstractGmsServiceBroker extends IGmsServiceBroker.Stub {
private static final String TAG = "GmsServiceBroker";
private final EnumSet<GmsService> supportedServices;
public AbstractGmsServiceBroker(EnumSet<GmsService> supportedServices) {
this.supportedServices = supportedServices;
}
@Deprecated
@Override
public void getPlusService(IGmsCallbacks callback, int versionCode, String packageName,
String authPackage, String[] scopes, String accountName, Bundle params)
throws RemoteException {
Bundle extras = params == null ? new Bundle() : params;
extras.putString("auth_package", authPackage);
callGetService(GmsService.PLUS, callback, versionCode, packageName, extras, accountName, scopes);
}
@Deprecated
@Override
public void getPanoramaService(IGmsCallbacks callback, int versionCode, String packageName,
Bundle params) throws RemoteException {
callGetService(GmsService.PANORAMA, callback, versionCode, packageName, params);
}
@Deprecated
@Override
public void getAppDataSearchService(IGmsCallbacks callback, int versionCode, String packageName)
throws RemoteException {
callGetService(GmsService.INDEX, callback, versionCode, packageName);
}
@Deprecated
@Override
public void getWalletService(IGmsCallbacks callback, int versionCode) throws RemoteException {
getWalletServiceWithPackageName(callback, versionCode, null);
}
@Deprecated
@Override
public void getPeopleService(IGmsCallbacks callback, int versionCode, String packageName,
Bundle params) throws RemoteException {
callGetService(GmsService.PEOPLE, callback, versionCode, packageName, params);
}
@Deprecated
@Override
public void getReportingService(IGmsCallbacks callback, int versionCode, String packageName,
Bundle params) throws RemoteException {
callGetService(GmsService.LOCATION_REPORTING, callback, versionCode, packageName, params);
}
@Deprecated
@Override
public void getLocationService(IGmsCallbacks callback, int versionCode, String packageName,
Bundle params) throws RemoteException {
callGetService(GmsService.LOCATION, callback, versionCode, packageName, params);
}
@Deprecated
@Override
public void getGoogleLocationManagerService(IGmsCallbacks callback, int versionCode,
String packageName, Bundle params) throws RemoteException {
callGetService(GmsService.LOCATION_MANAGER, callback, versionCode, packageName, params);
}
@Deprecated
@Override
public void getGamesService(IGmsCallbacks callback, int versionCode, String packageName,
String accountName, String[] scopes, String gamePackageName,
IBinder popupWindowToken, String desiredLocale, Bundle params)
throws RemoteException {
Bundle extras = params == null ? new Bundle() : params;
extras.putString("com.google.android.gms.games.key.gamePackageName", gamePackageName);
extras.putString("com.google.android.gms.games.key.desiredLocale", desiredLocale);
extras.putParcelable("com.google.android.gms.games.key.popupWindowToken", new BinderWrapper(popupWindowToken));
callGetService(GmsService.GAMES, callback, versionCode, packageName, extras, accountName, scopes);
}
@Deprecated
@Override
public void getAppStateService(IGmsCallbacks callback, int versionCode, String packageName,
String accountName, String[] scopes) throws RemoteException {
callGetService(GmsService.APPSTATE, callback, versionCode, packageName, null, accountName, scopes);
}
@Deprecated
@Override
public void getPlayLogService(IGmsCallbacks callback, int versionCode, String packageName,
Bundle params) throws RemoteException {
callGetService(GmsService.PLAY_LOG, callback, versionCode, packageName, params);
}
@Deprecated
@Override
public void getAdMobService(IGmsCallbacks callback, int versionCode, String packageName,
Bundle params) throws RemoteException {
callGetService(GmsService.ADREQUEST, callback, versionCode, packageName, params);
}
@Deprecated
@Override
public void getDroidGuardService(IGmsCallbacks callback, int versionCode, String packageName,
Bundle params) throws RemoteException {
callGetService(GmsService.DROIDGUARD, callback, versionCode, packageName, params);
}
@Deprecated
@Override
public void getLockboxService(IGmsCallbacks callback, int versionCode, String packageName,
Bundle params) throws RemoteException {
callGetService(GmsService.LOCKBOX, callback, versionCode, packageName, params);
}
@Deprecated
@Override
public void getCastMirroringService(IGmsCallbacks callback, int versionCode, String packageName,
Bundle params) throws RemoteException {
callGetService(GmsService.CAST_MIRRORING, callback, versionCode, packageName, params);
}
@Deprecated
@Override
public void getNetworkQualityService(IGmsCallbacks callback, int versionCode,
String packageName, Bundle params) throws RemoteException {
callGetService(GmsService.NETWORK_QUALITY, callback, versionCode, packageName, params);
}
@Deprecated
@Override
public void getGoogleIdentityService(IGmsCallbacks callback, int versionCode,
String packageName, Bundle params) throws RemoteException {
callGetService(GmsService.ACCOUNT, callback, versionCode, packageName, params);
}
@Deprecated
@Override
public void getGoogleFeedbackService(IGmsCallbacks callback, int versionCode,
String packageName, Bundle params) throws RemoteException {
callGetService(GmsService.FEEDBACK, callback, versionCode, packageName, params);
}
@Deprecated
@Override
public void getCastService(IGmsCallbacks callback, int versionCode, String packageName,
IBinder binder, Bundle params) throws RemoteException {
callGetService(GmsService.CAST, callback, versionCode, packageName, params);
}
@Deprecated
@Override
public void getDriveService(IGmsCallbacks callback, int versionCode, String packageName,
String[] scopes, String accountName, Bundle params) throws RemoteException {
callGetService(GmsService.DRIVE, callback, versionCode, packageName, params, accountName, scopes);
}
@Deprecated
@Override
public void getLightweightAppDataSearchService(IGmsCallbacks callback, int versionCode,
String packageName) throws RemoteException {
callGetService(GmsService.LIGHTWEIGHT_INDEX, callback, versionCode, packageName);
}
@Deprecated
@Override
public void getSearchAdministrationService(IGmsCallbacks callback, int versionCode,
String packageName) throws RemoteException {
callGetService(GmsService.SEARCH_ADMINISTRATION, callback, versionCode, packageName);
}
@Deprecated
@Override
public void getAutoBackupService(IGmsCallbacks callback, int versionCode, String packageName,
Bundle params) throws RemoteException {
callGetService(GmsService.PHOTO_AUTO_BACKUP, callback, versionCode, packageName, params);
}
@Deprecated
@Override
public void getAddressService(IGmsCallbacks callback, int versionCode, String packageName)
throws RemoteException {
callGetService(GmsService.ADDRESS, callback, versionCode, packageName);
}
@Deprecated
@Override
public void getWalletServiceWithPackageName(IGmsCallbacks callback, int versionCode, String packageName) throws RemoteException {
callGetService(GmsService.WALLET, callback, versionCode, packageName);
}
private void callGetService(GmsService service, IGmsCallbacks callback, int gmsVersion,
String packageName) throws RemoteException {
callGetService(service, callback, gmsVersion, packageName, null);
}
private void callGetService(GmsService service, IGmsCallbacks callback, int gmsVersion,
String packageName, Bundle extras) throws RemoteException {
callGetService(service, callback, gmsVersion, packageName, extras, null, null);
}
private void callGetService(GmsService service, IGmsCallbacks callback, int gmsVersion, String packageName, Bundle extras, String accountName, String[] scopes) throws RemoteException {
GetServiceRequest request = new GetServiceRequest(service.SERVICE_ID);
request.gmsVersion = gmsVersion;
request.packageName = packageName;
request.extras = extras;
request.account = accountName == null ? null : new Account(accountName, AuthConstants.DEFAULT_ACCOUNT_TYPE);
request.scopes = scopes == null ? null : scopesFromStringArray(scopes);
getService(callback, request);
}
private Scope[] scopesFromStringArray(String[] arr) {
Scope[] scopes = new Scope[arr.length];
for (int i = 0; i < arr.length; i++) {
scopes[i] = new Scope(arr[i]);
}
return scopes;
}
@Override
public void getService(IGmsCallbacks callback, GetServiceRequest request) throws RemoteException {
GmsService gmsService = GmsService.byServiceId(request.serviceId);
if ((supportedServices.contains(gmsService)) || supportedServices.contains(GmsService.ANY)) {
handleServiceRequest(callback, request, gmsService);
} else {
Log.d(TAG, "Service not supported: " + request);
throw new IllegalArgumentException("Service not supported: " + request.serviceId);
}
}
public abstract void handleServiceRequest(IGmsCallbacks callback, GetServiceRequest request, GmsService service) throws RemoteException;
@Override
public void validateAccount(IGmsCallbacks callback, ValidateAccountRequest request) throws RemoteException {
throw new IllegalArgumentException("ValidateAccountRequest not supported");
}
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
if (super.onTransact(code, data, reply, flags)) return true;
Log.d(TAG, "onTransact [unknown]: " + code + ", " + data + ", " + flags);
return false;
}
}

View file

@ -0,0 +1,73 @@
/*
* 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;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import androidx.lifecycle.LifecycleService;
import com.google.android.gms.common.internal.GetServiceRequest;
import com.google.android.gms.common.internal.IGmsCallbacks;
import com.google.android.gms.common.internal.IGmsServiceBroker;
import org.microg.gms.common.GmsService;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.EnumSet;
public abstract class BaseService extends LifecycleService {
private final IGmsServiceBroker broker;
private final EnumSet<GmsService> services;
protected final String TAG;
public BaseService(String tag, GmsService supportedService, GmsService... supportedServices) {
this.TAG = tag;
services = EnumSet.of(supportedService);
services.addAll(Arrays.asList(supportedServices));
broker = new AbstractGmsServiceBroker(services) {
@Override
public void handleServiceRequest(IGmsCallbacks callback, GetServiceRequest request, GmsService service) throws RemoteException {
try {
request.extras.keySet(); // call to unparcel()
} catch (Exception e) {
// Sometimes we need to define the correct ClassLoader before unparcel(). Ignore those.
}
Log.d(TAG, "bound by: " + request);
BaseService.this.handleServiceRequest(callback, request, service);
}
};
}
@Override
public IBinder onBind(Intent intent) {
super.onBind(intent);
Log.d(TAG, "onBind: " + intent);
return broker.asBinder();
}
@Override
protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
writer.println(TAG + " providing services " + services.toString());
}
public abstract void handleServiceRequest(IGmsCallbacks callback, GetServiceRequest request, GmsService service) throws RemoteException;
}

View file

@ -0,0 +1,37 @@
/*
* 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;
import android.os.RemoteException;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.CommonStatusCodes;
import com.google.android.gms.common.internal.GetServiceRequest;
import com.google.android.gms.common.internal.IGmsCallbacks;
import org.microg.gms.common.GmsService;
public class DummyService extends BaseService {
public DummyService() {
super("GmsDummySvc", GmsService.ANY);
}
@Override
public void handleServiceRequest(IGmsCallbacks callback, GetServiceRequest request, GmsService service) throws RemoteException {
callback.onPostInitComplete(ConnectionResult.API_DISABLED, null, null);
}
}

View file

@ -0,0 +1,263 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.auth;
import android.content.Context;
import org.microg.gms.checkin.LastCheckinInfo;
import org.microg.gms.profile.Build;
import org.microg.gms.common.Constants;
import org.microg.gms.common.HttpFormClient;
import org.microg.gms.common.Utils;
import org.microg.gms.profile.ProfileManager;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import static org.microg.gms.common.HttpFormClient.RequestContent;
import static org.microg.gms.common.HttpFormClient.RequestHeader;
public class AuthRequest extends HttpFormClient.Request {
private static final String SERVICE_URL = "https://android.googleapis.com/auth";
private static final String USER_AGENT = "GoogleAuth/1.4 (%s %s); gzip";
@RequestHeader("User-Agent")
private String userAgent;
@RequestHeader("app")
@RequestContent("app")
public String app;
@RequestContent("client_sig")
public String appSignature;
@RequestContent("callerPkg")
public String caller;
@RequestContent("callerSig")
public String callerSignature;
@RequestHeader(value = "device", nullPresent = true)
@RequestContent(value = "androidId", nullPresent = true)
public String androidIdHex;
@RequestContent("sdk_version")
public int sdkVersion;
@RequestContent("device_country")
public String countryCode;
@RequestContent("operatorCountry")
public String operatorCountryCode;
@RequestContent("lang")
public String locale;
@RequestContent("google_play_services_version")
public int gmsVersion = Constants.GMS_VERSION_CODE;
@RequestContent("accountType")
public String accountType;
@RequestContent("Email")
public String email;
@RequestContent("service")
public String service;
@RequestContent("source")
public String source;
@RequestContent({"is_called_from_account_manager", "_opt_is_called_from_account_manager"})
public boolean isCalledFromAccountManager;
@RequestContent("Token")
public String token;
@RequestContent("system_partition")
public boolean systemPartition;
@RequestContent("get_accountid")
public boolean getAccountId;
@RequestContent("ACCESS_TOKEN")
public boolean isAccessToken;
@RequestContent("droidguard_results")
public String droidguardResults;
@RequestContent("has_permission")
public boolean hasPermission;
@RequestContent("add_account")
public boolean addAccount;
@RequestContent("delegation_type")
public String delegationType;
@RequestContent("delegatee_user_id")
public String delegateeUserId;
@RequestContent("oauth2_foreground")
public String oauth2Foreground;
@RequestContent("token_request_options")
public String tokenRequestOptions;
@RequestContent("it_caveat_types")
public String itCaveatTypes;
@RequestContent("check_email")
public boolean checkEmail;
@RequestContent("request_visible_actions")
public String requestVisibleActions;
@RequestContent("oauth2_prompt")
public String oauth2Prompt;
@RequestContent("oauth2_include_profile")
public String oauth2IncludeProfile;
@RequestContent("oauth2_include_email")
public String oauth2IncludeEmail;
@HttpFormClient.RequestContentDynamic
public Map<Object, Object> dynamicFields;
public String deviceName;
public String buildVersion;
@Override
protected void prepare() {
userAgent = String.format(USER_AGENT, deviceName, buildVersion);
}
public AuthRequest build(Context context) {
ProfileManager.ensureInitialized(context);
sdkVersion = Build.VERSION.SDK_INT;
deviceName = Build.DEVICE;
buildVersion = Build.ID;
return this;
}
public AuthRequest source(String source) {
this.source = source;
return this;
}
public AuthRequest locale(Locale locale) {
this.locale = locale.toString();
this.countryCode = locale.getCountry().toLowerCase();
this.operatorCountryCode = locale.getCountry().toLowerCase();
return this;
}
public AuthRequest fromContext(Context context) {
build(context);
locale(Utils.getLocale(context));
if (AuthPrefs.shouldIncludeAndroidId(context)) {
androidIdHex = Long.toHexString(LastCheckinInfo.read(context).getAndroidId());
}
if (AuthPrefs.shouldStripDeviceName(context)) {
deviceName = "";
buildVersion = "";
}
return this;
}
public AuthRequest email(String email) {
this.email = email;
return this;
}
public AuthRequest token(String token) {
this.token = token;
return this;
}
public AuthRequest service(String service) {
this.service = service;
return this;
}
public AuthRequest app(String app, String appSignature) {
this.app = app;
this.appSignature = appSignature;
return this;
}
public AuthRequest appIsGms() {
return app(Constants.GMS_PACKAGE_NAME, Constants.GMS_PACKAGE_SIGNATURE_SHA1);
}
public AuthRequest callerIsGms() {
return caller(Constants.GMS_PACKAGE_NAME, Constants.GMS_PACKAGE_SIGNATURE_SHA1);
}
public AuthRequest callerIsApp() {
return caller(app, appSignature);
}
public AuthRequest caller(String caller, String callerSignature) {
this.caller = caller;
this.callerSignature = callerSignature;
return this;
}
public AuthRequest calledFromAccountManager() {
isCalledFromAccountManager = true;
return this;
}
public AuthRequest addAccount() {
addAccount = true;
return this;
}
public AuthRequest systemPartition(boolean systemPartition) {
this.systemPartition = systemPartition;
return this;
}
public AuthRequest hasPermission(boolean hasPermission) {
this.hasPermission = hasPermission;
return this;
}
public AuthRequest getAccountId() {
getAccountId = true;
return this;
}
public AuthRequest isAccessToken() {
isAccessToken = true;
return this;
}
public AuthRequest droidguardResults(String droidguardResults) {
this.droidguardResults = droidguardResults;
return this;
}
public AuthRequest delegation(int delegationType, String delegateeUserId) {
this.delegationType = delegationType == 0 ? null : Integer.toString(delegationType);
this.delegateeUserId = delegateeUserId;
return this;
}
public AuthRequest oauth2Foreground(String oauth2Foreground) {
this.oauth2Foreground = oauth2Foreground;
return this;
}
public AuthRequest tokenRequestOptions(String tokenRequestOptions) {
this.tokenRequestOptions = tokenRequestOptions;
return this;
}
public AuthRequest oauth2IncludeProfile(String oauth2IncludeProfile) {
this.oauth2IncludeProfile = oauth2IncludeProfile;
return this;
}
public AuthRequest oauth2IncludeEmail(String oauth2IncludeEmail) {
this.oauth2IncludeEmail = oauth2IncludeEmail;
return this;
}
public AuthRequest oauth2Prompt(String oauth2Prompt) {
this.oauth2Prompt = oauth2Prompt;
return this;
}
public AuthRequest itCaveatTypes(String itCaveatTypes) {
this.itCaveatTypes = itCaveatTypes;
return this;
}
public AuthRequest putDynamicFiledMap(Map<Object, Object> dynamicFields) {
this.dynamicFields = dynamicFields;
return this;
}
public AuthResponse getResponse() throws IOException {
return HttpFormClient.request(SERVICE_URL, this, AuthResponse.class);
}
public void getResponseAsync(HttpFormClient.Callback<AuthResponse> callback) {
HttpFormClient.requestAsync(SERVICE_URL, this, AuthResponse.class, callback);
}
}

View file

@ -0,0 +1,138 @@
/*
* 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.auth;
import android.util.Log;
import java.lang.reflect.Field;
import static org.microg.gms.common.HttpFormClient.ResponseField;
public class AuthResponse {
private static final String TAG = "GmsAuthResponse";
@ResponseField("SID")
public String Sid;
@ResponseField("LSID")
public String LSid;
@ResponseField("Auth")
public String auth;
@ResponseField("Token")
public String token;
@ResponseField("Email")
public String email;
@ResponseField("services")
public String services;
@ResponseField("GooglePlusUpgrade")
public boolean isGooglePlusUpgrade;
@ResponseField("PicasaUser")
public String picasaUserName;
@ResponseField("RopText")
public String ropText;
@ResponseField("RopRevision")
public int ropRevision;
@ResponseField("firstName")
public String firstName;
@ResponseField("lastName")
public String lastName;
@ResponseField("issueAdvice")
public String issueAdvice;
@ResponseField("accountId")
public String accountId;
@ResponseField("Expiry")
public long expiry = -1;
@ResponseField("storeConsentRemotely")
public boolean storeConsentRemotely = true;
@ResponseField("Permission")
public String permission;
@ResponseField("ScopeConsentDetails")
public String scopeConsentDetails;
@ResponseField("ConsentDataBase64")
public String consentDataBase64;
@ResponseField("grantedScopes")
public String grantedScopes;
@ResponseField("itMetadata")
public String itMetadata;
@ResponseField("ResolutionDataBase64")
public String resolutionDataBase64;
@ResponseField("it")
public String auths;
@ResponseField("capabilities")
public String capabilities;
@ResponseField("ExpiresInDurationSec")
public int expiresInDurationSec;
public static AuthResponse parse(String result) {
AuthResponse response = new AuthResponse();
String[] entries = result.split("\n");
for (String s : entries) {
String[] keyValuePair = s.split("=", 2);
String key = keyValuePair[0].trim();
String value = keyValuePair[1].trim();
try {
for (Field field : AuthResponse.class.getDeclaredFields()) {
if (field.isAnnotationPresent(ResponseField.class) &&
key.equals(field.getAnnotation(ResponseField.class).value())) {
if (field.getType().equals(String.class)) {
field.set(response, value);
} else if (field.getType().equals(boolean.class)) {
field.setBoolean(response, value.equals("1"));
} else if (field.getType().equals(long.class)) {
field.setLong(response, Long.parseLong(value));
} else if (field.getType().equals(int.class)) {
field.setInt(response, Integer.parseInt(value));
}
}
}
} catch (Exception e) {
Log.w(TAG, e);
}
}
return response;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("AuthResponse{");
sb.append("auth='").append(auth).append('\'');
if (Sid != null) sb.append(", Sid='").append(Sid).append('\'');
if (LSid != null) sb.append(", LSid='").append(LSid).append('\'');
if (token != null) sb.append(", token='").append(token).append('\'');
if (email != null) sb.append(", email='").append(email).append('\'');
if (services != null) sb.append(", services='").append(services).append('\'');
if (isGooglePlusUpgrade) sb.append(", isGooglePlusUpgrade=").append(isGooglePlusUpgrade);
if (picasaUserName != null) sb.append(", picasaUserName='").append(picasaUserName).append('\'');
if (ropText != null) sb.append(", ropText='").append(ropText).append('\'');
if (ropRevision != 0) sb.append(", ropRevision=").append(ropRevision);
if (firstName != null) sb.append(", firstName='").append(firstName).append('\'');
if (lastName != null) sb.append(", lastName='").append(lastName).append('\'');
if (issueAdvice != null) sb.append(", issueAdvice='").append(issueAdvice).append('\'');
if (accountId != null) sb.append(", accountId='").append(accountId).append('\'');
if (expiry != -1) sb.append(", expiry=").append(expiry);
if (!storeConsentRemotely) sb.append(", storeConsentRemotely=").append(storeConsentRemotely);
if (permission != null) sb.append(", permission='").append(permission).append('\'');
if (scopeConsentDetails != null) sb.append(", scopeConsentDetails='").append(scopeConsentDetails).append('\'');
if (consentDataBase64 != null) sb.append(", consentDataBase64='").append(consentDataBase64).append('\'');
if (auths != null) sb.append(", auths='").append(auths).append('\'');
if (itMetadata != null) sb.append(", itMetadata='").append(itMetadata).append('\'');
if (resolutionDataBase64 != null) sb.append(", resolutionDataBase64='").append(resolutionDataBase64).append('\'');
if (capabilities != null) sb.append(", capabilitites='").append(capabilities).append('\'');
if (expiresInDurationSec != 0) sb.append(", expiresInDurationSec='").append(expiresInDurationSec).append('\'');
sb.append('}');
return sb.toString();
}
}

View file

@ -0,0 +1,196 @@
/*
* 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.common;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.ConfigurationInfo;
import android.content.pm.FeatureInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.opengl.GLES10;
import android.util.DisplayMetrics;
import org.microg.gms.profile.Build;
import org.microg.gms.profile.ProfileManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
import static android.os.Build.VERSION.SDK_INT;
public class DeviceConfiguration {
public List<String> availableFeatures;
public int densityDpi;
public double diagonalInch;
public int glEsVersion;
public List<String> glExtensions;
public boolean hasFiveWayNavigation;
public boolean hasHardKeyboard;
public int heightPixels;
public int keyboardType;
public List<String> locales;
public List<String> nativePlatforms;
public int navigation;
public int screenLayout;
public List<String> sharedLibraries;
public int touchScreen;
public int widthPixels;
public DeviceConfiguration(Context context) {
ProfileManager.ensureInitialized(context);
ConfigurationInfo configurationInfo = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getDeviceConfigurationInfo();
touchScreen = configurationInfo.reqTouchScreen;
keyboardType = configurationInfo.reqKeyboardType;
navigation = configurationInfo.reqNavigation;
Configuration configuration = context.getResources().getConfiguration();
screenLayout = configuration.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
hasHardKeyboard = (configurationInfo.reqInputFeatures & ConfigurationInfo.INPUT_FEATURE_HARD_KEYBOARD) > 0;
hasFiveWayNavigation = (configurationInfo.reqInputFeatures & ConfigurationInfo.INPUT_FEATURE_FIVE_WAY_NAV) > 0;
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
densityDpi = displayMetrics.densityDpi;
glEsVersion = configurationInfo.reqGlEsVersion;
PackageManager packageManager = context.getPackageManager();
String[] systemSharedLibraryNames = packageManager.getSystemSharedLibraryNames();
sharedLibraries = new ArrayList<String>();
if (systemSharedLibraryNames != null) sharedLibraries.addAll(Arrays.asList(systemSharedLibraryNames));
for (String s : new String[]{"com.google.android.maps", "com.google.android.media.effects", "com.google.widevine.software.drm"}) {
if (!sharedLibraries.contains(s)) {
sharedLibraries.add(s);
}
}
Collections.sort(sharedLibraries);
availableFeatures = new ArrayList<String>();
if (packageManager.getSystemAvailableFeatures() != null) {
for (FeatureInfo featureInfo : packageManager.getSystemAvailableFeatures()) {
if (featureInfo != null && featureInfo.name != null) availableFeatures.add(featureInfo.name);
}
}
Collections.sort(availableFeatures);
this.nativePlatforms = getNativePlatforms();
widthPixels = displayMetrics.widthPixels;
heightPixels = displayMetrics.heightPixels;
diagonalInch = Math.sqrt(
Math.pow(widthPixels / displayMetrics.xdpi, 2) +
Math.pow(heightPixels / displayMetrics.ydpi, 2)
);
locales = getLocales(context);
Set<String> glExtensions = new HashSet<String>();
addEglExtensions(glExtensions);
this.glExtensions = new ArrayList<String>(glExtensions);
Collections.sort(this.glExtensions);
}
@SuppressLint("GetLocales")
private static List<String> getLocales(Context context) {
List<String> locales = new ArrayList<String>();
if (SDK_INT >= 21) {
locales.addAll(Arrays.asList(context.getAssets().getLocales()));
} else {
locales.add("en-US");
}
for (int i = 0; i < locales.size(); i++) {
locales.set(i, locales.get(i).replace("-", "_"));
}
Collections.sort(locales);
return locales;
}
@SuppressWarnings({"deprecation", "InlinedApi"})
private static List<String> getNativePlatforms() {
List<String> nativePlatforms;
if (Build.VERSION.SDK_INT >= 21) {
return Arrays.asList(Build.SUPPORTED_ABIS);
} else {
nativePlatforms = new ArrayList<String>();
nativePlatforms.add(Build.CPU_ABI);
if (Build.CPU_ABI2 != null && !Build.CPU_ABI2.equals("unknown"))
nativePlatforms.add(Build.CPU_ABI2);
return nativePlatforms;
}
}
private static void addEglExtensions(Set<String> glExtensions) {
EGL10 egl10 = (EGL10) EGLContext.getEGL();
if (egl10 != null) {
EGLDisplay display = egl10.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
egl10.eglInitialize(display, new int[2]);
int cf[] = new int[1];
if (egl10.eglGetConfigs(display, null, 0, cf)) {
EGLConfig[] configs = new EGLConfig[cf[0]];
if (egl10.eglGetConfigs(display, configs, cf[0], cf)) {
int[] a1 =
new int[]{EGL10.EGL_WIDTH, EGL10.EGL_PBUFFER_BIT, EGL10.EGL_HEIGHT, EGL10.EGL_PBUFFER_BIT,
EGL10.EGL_NONE};
int[] a2 = new int[]{12440, EGL10.EGL_PIXMAP_BIT, EGL10.EGL_NONE};
int[] a3 = new int[1];
for (int i = 0; i < cf[0]; i++) {
egl10.eglGetConfigAttrib(display, configs[i], EGL10.EGL_CONFIG_CAVEAT, a3);
if (a3[0] != EGL10.EGL_SLOW_CONFIG) {
egl10.eglGetConfigAttrib(display, configs[i], EGL10.EGL_SURFACE_TYPE, a3);
if ((1 & a3[0]) != 0) {
egl10.eglGetConfigAttrib(display, configs[i], EGL10.EGL_RENDERABLE_TYPE, a3);
if ((1 & a3[0]) != 0) {
addExtensionsForConfig(egl10, display, configs[i], a1, null, glExtensions);
}
if ((4 & a3[0]) != 0) {
addExtensionsForConfig(egl10, display, configs[i], a1, a2, glExtensions);
}
}
}
}
}
}
egl10.eglTerminate(display);
}
}
private static void addExtensionsForConfig(EGL10 egl10, EGLDisplay egldisplay, EGLConfig eglconfig, int ai[],
int ai1[], Set<String> set) {
EGLContext eglcontext = egl10.eglCreateContext(egldisplay, eglconfig, EGL10.EGL_NO_CONTEXT, ai1);
if (eglcontext != EGL10.EGL_NO_CONTEXT) {
javax.microedition.khronos.egl.EGLSurface eglsurface =
egl10.eglCreatePbufferSurface(egldisplay, eglconfig, ai);
if (eglsurface == EGL10.EGL_NO_SURFACE) {
egl10.eglDestroyContext(egldisplay, eglcontext);
} else {
egl10.eglMakeCurrent(egldisplay, eglsurface, eglsurface, eglcontext);
String s = GLES10.glGetString(7939);
if (s != null && !s.isEmpty()) {
String as[] = s.split(" ");
int i = as.length;
for (int j = 0; j < i; j++) {
set.add(as[j]);
}
}
egl10.eglMakeCurrent(egldisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
egl10.eglDestroySurface(egldisplay, eglsurface);
egl10.eglDestroyContext(egldisplay, eglcontext);
}
}
}
}

View file

@ -0,0 +1,61 @@
/*
* 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.common;
import java.util.Random;
public class DeviceIdentifier {
public String wifiMac = randomMacAddress(); // TODO: static
public String meid = randomMeid();
public String esn;
private static String randomMacAddress() {
String mac = "b407f9";
Random rand = new Random();
for (int i = 0; i < 6; i++) {
mac += Integer.toString(rand.nextInt(16), 16);
}
return mac;
}
private static String randomMeid() {
// http://en.wikipedia.org/wiki/International_Mobile_Equipment_Identity
// We start with a known base, and generate random MEID
String meid = "35503104";
Random rand = new Random();
for (int i = 0; i < 6; i++) {
meid += Integer.toString(rand.nextInt(10));
}
// Luhn algorithm (check digit)
int sum = 0;
for (int i = 0; i < meid.length(); i++) {
int c = Integer.parseInt(String.valueOf(meid.charAt(i)));
if ((meid.length() - i - 1) % 2 == 0) {
c *= 2;
c = c % 10 + c / 10;
}
sum += c;
}
final int check = (100 - sum) % 10;
meid += Integer.toString(check);
return meid;
}
}

View file

@ -0,0 +1,106 @@
package org.microg.gms.common;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.os.PowerManager;
import android.util.Log;
import androidx.annotation.RequiresApi;
import org.microg.gms.base.core.R;
import static android.os.Build.VERSION.SDK_INT;
public class ForegroundServiceContext extends ContextWrapper {
private static final String TAG = "ForegroundService";
public static final String EXTRA_FOREGROUND = "foreground";
public ForegroundServiceContext(Context base) {
super(base);
}
@Override
public ComponentName startService(Intent service) {
if (SDK_INT >= 26 && !isIgnoringBatteryOptimizations()) {
Log.d(TAG, "Starting in foreground mode.");
service.putExtra(EXTRA_FOREGROUND, true);
return super.startForegroundService(service);
}
return super.startService(service);
}
@RequiresApi(23)
private boolean isIgnoringBatteryOptimizations() {
PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
return powerManager.isIgnoringBatteryOptimizations(getPackageName());
}
private static String getServiceName(Service service) {
String serviceName = null;
try {
ForegroundServiceInfo annotation = service.getClass().getAnnotation(ForegroundServiceInfo.class);
if (annotation != null) {
serviceName = annotation.value();
if (annotation.res() != 0) {
try {
serviceName = service.getString(annotation.res());
} catch (Exception ignored) {
}
}
if (!annotation.resName().isEmpty() && !annotation.resPackage().isEmpty()) {
try {
serviceName = service.getString(service.getResources().getIdentifier(annotation.resName(), "string", annotation.resPackage()));
} catch (Exception ignored) {
}
}
}
} catch (Exception ignored) {
}
if (serviceName == null) {
serviceName = service.getClass().getSimpleName();
}
return serviceName;
}
public static void completeForegroundService(Service service, Intent intent, String tag) {
if (intent != null && intent.getBooleanExtra(EXTRA_FOREGROUND, false) && SDK_INT >= 26) {
String serviceName = getServiceName(service);
Log.d(tag, "Started " + serviceName + " in foreground mode.");
try {
Notification notification = buildForegroundNotification(service, serviceName);
service.startForeground(serviceName.hashCode(), notification);
Log.d(tag, "Notification: " + notification.toString());
} catch (Exception e) {
Log.w(tag, e);
}
}
}
@RequiresApi(26)
private static Notification buildForegroundNotification(Context context, String serviceName) {
NotificationChannel channel = new NotificationChannel("foreground-service", "Foreground Service", NotificationManager.IMPORTANCE_NONE);
channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
channel.setShowBadge(false);
channel.setVibrationPattern(new long[]{0});
context.getSystemService(NotificationManager.class).createNotificationChannel(channel);
String appTitle = context.getApplicationInfo().loadLabel(context.getPackageManager()).toString();
String notifyTitle = context.getString(R.string.foreground_service_notification_title);
String firstLine = context.getString(R.string.foreground_service_notification_text, serviceName);
String secondLine = context.getString(R.string.foreground_service_notification_big_text, appTitle);
Log.d(TAG, notifyTitle + " // " + firstLine + " // " + secondLine);
return new Notification.Builder(context, channel.getId())
.setOngoing(true)
.setSmallIcon(R.drawable.ic_background_notify)
.setContentTitle(notifyTitle)
.setContentText(firstLine)
.setStyle(new Notification.BigTextStyle().bigText(firstLine + "\n" + secondLine))
.build();
}
}

View file

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.common;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ForegroundServiceInfo {
String value();
@Deprecated
int res() default 0;
String resName() default "";
String resPackage() default "";
}

View file

@ -0,0 +1,278 @@
/*
* 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.common;
import android.net.Uri;
import android.util.Log;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Map;
public class HttpFormClient {
private static final String TAG = "GmsHttpFormClient";
public static <T> T request(String url, Request request, Class<T> tClass) throws IOException {
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestMethod("POST");
connection.setDoInput(true);
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
StringBuilder content = new StringBuilder();
request.prepare();
for (Field field : request.getClass().getDeclaredFields()) {
try {
field.setAccessible(true);
Object objVal = field.get(request);
if (field.isAnnotationPresent(RequestContentDynamic.class)) {
Map<String, String> contentParams = (Map<String, String>) objVal;
for (Map.Entry<String, String> param : contentParams.entrySet()) {
appendParam(content, param.getKey(), param.getValue());
}
continue;
}
String value = objVal != null ? String.valueOf(objVal) : null;
Boolean boolVal = null;
if (field.getType().equals(boolean.class)) {
boolVal = field.getBoolean(request);
}
if (field.isAnnotationPresent(RequestHeader.class)) {
RequestHeader annotation = field.getAnnotation(RequestHeader.class);
value = valueFromBoolVal(value, boolVal, annotation.truePresent(), annotation.falsePresent());
if (value != null || annotation.nullPresent()) {
for (String key : annotation.value()) {
connection.setRequestProperty(key, String.valueOf(value));
}
}
}
if (field.isAnnotationPresent(RequestContent.class)) {
RequestContent annotation = field.getAnnotation(RequestContent.class);
value = valueFromBoolVal(value, boolVal, annotation.truePresent(), annotation.falsePresent());
if (value != null || annotation.nullPresent()) {
for (String key : annotation.value()) {
appendParam(content, key, value);
}
}
}
} catch (Exception ignored) {
}
}
Log.d(TAG, "-- Request --\n" + content);
String replace = content.toString().trim().replace("\n", "");
OutputStream os = connection.getOutputStream();
os.write(replace.trim().getBytes());
os.close();
if (connection.getResponseCode() != 200) {
String error = connection.getResponseMessage();
try {
error = new String(Utils.readStreamToEnd(connection.getErrorStream()));
} catch (IOException e) {
// Ignore
}
throw new NotOkayException(error);
}
String result = new String(Utils.readStreamToEnd(connection.getInputStream()));
Log.d(TAG, "-- Response --\n" + result);
return parseResponse(tClass, connection, result);
}
private static String valueFromBoolVal(String value, Boolean boolVal, boolean truePresent, boolean falsePresent) {
if (boolVal != null) {
if (boolVal && truePresent) {
return "1";
} else if (!boolVal && falsePresent) {
return "0";
} else {
return null;
}
} else {
return value;
}
}
private static void appendParam(StringBuilder content, String key, String value) {
if (content.length() > 0)
content.append("&");
if (key.equals("token_request_options")) {
content.append(Uri.encode(key)).append("=").append(value);
} else {
content.append(Uri.encode(key)).append("=").append(Uri.encode(String.valueOf(value)));
}
}
private static <T> T parseResponse(Class<T> tClass, HttpURLConnection connection, String result) throws IOException {
Map<String, List<String>> headerFields = connection.getHeaderFields();
T response;
try {
response = tClass.getConstructor().newInstance();
} catch (Exception e) {
return null;
}
String[] entries = result.split("\n");
for (String s : entries) {
String[] keyValuePair = s.split("=", 2);
String key = keyValuePair[0].trim();
String value = keyValuePair[1].trim();
boolean matched = false;
try {
for (Field field : tClass.getDeclaredFields()) {
if (field.isAnnotationPresent(ResponseField.class) &&
key.equals(field.getAnnotation(ResponseField.class).value())) {
field.setAccessible(true);
matched = true;
if (field.getType().equals(String.class)) {
field.set(response, value);
} else if (field.getType().equals(boolean.class)) {
field.setBoolean(response, value.equals("1"));
} else if (field.getType().equals(long.class)) {
field.setLong(response, Long.parseLong(value));
} else if (field.getType().equals(int.class)) {
field.setInt(response, Integer.parseInt(value));
}
}
}
} catch (Exception e) {
Log.w(TAG, e);
}
if (!matched) {
Log.w(TAG, "Response line '" + s + "' not processed");
}
}
for (Field field : tClass.getDeclaredFields()) {
if (field.isAnnotationPresent(ResponseHeader.class)) {
List<String> strings = headerFields.get(field.getAnnotation(ResponseHeader.class).value());
if (strings == null || strings.size() != 1) continue;
String value = strings.get(0);
try {
field.setAccessible(true);
if (field.getType().equals(String.class)) {
field.set(response, value);
} else if (field.getType().equals(boolean.class)) {
field.setBoolean(response, value.equals("1"));
} else if (field.getType().equals(long.class)) {
field.setLong(response, Long.parseLong(value));
} else if (field.getType().equals(int.class)) {
field.setInt(response, Integer.parseInt(value));
}
} catch (Exception e) {
Log.w(TAG, e);
}
}
if (field.isAnnotationPresent(ResponseStatusCode.class) && field.getType() == int.class) {
try {
field.setAccessible(true);
field.setInt(response, connection.getResponseCode());
} catch (IllegalAccessException e) {
Log.w(TAG, e);
}
}
if (field.isAnnotationPresent(ResponseStatusText.class) && field.getType() == String.class) {
try {
field.setAccessible(true);
field.set(response, connection.getResponseMessage());
} catch (IllegalAccessException e) {
Log.w(TAG, e);
}
}
}
return response;
}
public static <T> void requestAsync(final String url, final Request request, final Class<T> tClass,
final Callback<T> callback) {
new Thread(() -> {
try {
callback.onResponse(request(url, request, tClass));
} catch (Exception e) {
callback.onException(e);
}
}).start();
}
public static abstract class Request {
protected void prepare() {
}
}
public interface Callback<T> {
void onResponse(T response);
void onException(Exception exception);
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface RequestHeader {
public String[] value();
public boolean truePresent() default true;
public boolean falsePresent() default false;
public boolean nullPresent() default false;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface RequestContent {
public String[] value();
public boolean truePresent() default true;
public boolean falsePresent() default false;
public boolean nullPresent() default false;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface RequestContentDynamic {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ResponseField {
public String value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ResponseHeader {
public String value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ResponseStatusCode {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ResponseStatusText {
}
}

View file

@ -0,0 +1,225 @@
/*
* 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.common;
import android.os.IInterface;
import android.util.Log;
import androidx.annotation.NonNull;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
public class MultiListenerProxy<T extends IInterface> implements InvocationHandler {
private static final String TAG = "GmsMultiListener";
public static <T extends IInterface> T get(Class<T> tClass, final Collection<T> listeners) {
return get(tClass, new CollectionListenerPool<T>(listeners));
}
public static <T extends IInterface> T get(Class<T> tClass, final ListenerPool<T> listenerPool) {
return (T) Proxy.newProxyInstance(tClass.getClassLoader(), new Class[]{tClass}, new MultiListenerProxy<T>(listenerPool));
}
private final ListenerPool<T> listeners;
private MultiListenerProxy(ListenerPool<T> listeners) {
this.listeners = listeners;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
for (T listener : new HashSet<T>(listeners)) {
try {
method.invoke(listener, args);
} catch (Exception e) {
Log.w(TAG, e);
listeners.remove(listener);
}
}
return null;
}
public static abstract class ListenerPool<T> implements Collection<T> {
@Override
public boolean addAll(Collection<? extends T> collection) {
return false;
}
@Override
public boolean add(T object) {
return false;
}
@Override
public boolean containsAll(Collection<?> collection) {
for (Object o : collection) {
if (!contains(o)) return false;
}
return true;
}
@Override
public boolean removeAll(Collection<?> collection) {
boolean x = true;
for (Object o : collection) {
if (!remove(o)) x = false;
}
return x;
}
@Override
public boolean retainAll(Collection<?> collection) {
return false;
}
@NonNull
@Override
public Object[] toArray() {
throw new IllegalArgumentException();
}
@NonNull
@Override
public <T1> T1[] toArray(T1[] array) {
throw new IllegalArgumentException();
}
}
private static class CollectionListenerPool<T> extends ListenerPool<T> {
private Collection<T> listeners;
public CollectionListenerPool(Collection<T> listeners) {
this.listeners = listeners;
}
@Override
public void clear() {
listeners.clear();
}
@Override
public boolean contains(Object object) {
return listeners.contains(object);
}
@Override
public boolean isEmpty() {
return listeners.isEmpty();
}
@NonNull
@Override
public Iterator<T> iterator() {
return listeners.iterator();
}
@Override
public boolean remove(Object object) {
return listeners.remove(object);
}
@Override
public int size() {
return listeners.size();
}
}
public static class MultiCollectionListenerPool<T> extends ListenerPool<T> {
private Collection<? extends Collection<T>> multiCol;
public MultiCollectionListenerPool(Collection<? extends Collection<T>> multiCol) {
this.multiCol = multiCol;
}
@Override
public void clear() {
for (Collection<T> ts : multiCol) {
ts.clear();
}
}
@Override
public boolean contains(Object object) {
for (Collection<T> ts : multiCol) {
if (ts.contains(object)) return true;
}
return false;
}
@Override
public boolean isEmpty() {
for (Collection<T> ts : multiCol) {
if (!ts.isEmpty()) return false;
}
return true;
}
@NonNull
@Override
public Iterator<T> iterator() {
final Iterator<? extends Collection<T>> interMed = multiCol.iterator();
return new Iterator<T>() {
private Iterator<T> med;
@Override
public boolean hasNext() {
while ((med == null || !med.hasNext()) && interMed.hasNext()) {
med = interMed.next().iterator();
}
return med != null && med.hasNext();
}
@Override
public T next() {
while (med == null || !med.hasNext()) {
med = interMed.next().iterator();
}
return med.next();
}
@Override
public void remove() {
med.remove();
}
};
}
@Override
public boolean remove(Object object) {
for (Collection<T> ts : multiCol) {
if (ts.remove(object)) return true;
}
return false;
}
@Override
public int size() {
int sum = 0;
for (Collection<T> ts : multiCol) {
sum += ts.size();
}
return sum;
}
}
}

View file

@ -0,0 +1,28 @@
/*
* 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.common;
import android.os.RemoteException;
import com.google.android.gms.common.internal.ICancelToken;
public class NonCancelToken extends ICancelToken.Stub {
@Override
public void cancel() throws RemoteException {
}
}

View file

@ -0,0 +1,20 @@
package org.microg.gms.common;
import java.io.IOException;
public class NotOkayException extends IOException {
public NotOkayException() {
}
public NotOkayException(String message) {
super(message);
}
public NotOkayException(String message, Throwable cause) {
super(message, cause);
}
public NotOkayException(Throwable cause) {
super(cause);
}
}

View file

@ -0,0 +1,393 @@
/*
* 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.common;
import android.app.ActivityManager;
import android.app.Application;
import android.app.PendingIntent;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.os.Binder;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.microg.gms.utils.ExtendedPackageInfo;
import java.lang.reflect.Method;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;
import static android.os.Build.VERSION.SDK_INT;
import static org.microg.gms.common.Constants.GMS_PACKAGE_SIGNATURE_SHA1;
import static org.microg.gms.common.Constants.GMS_SECONDARY_PACKAGE_SIGNATURE_SHA1;
public class PackageUtils {
private static final String GOOGLE_PLATFORM_KEY = GMS_PACKAGE_SIGNATURE_SHA1;
private static final String GOOGLE_PLATFORM_KEY_2 = GMS_SECONDARY_PACKAGE_SIGNATURE_SHA1;
private static final String GOOGLE_APP_KEY = "24bb24c05e47e0aefa68a58a766179d9b613a600";
private static final String GOOGLE_LEGACY_KEY = "58e1c4133f7441ec3d2c270270a14802da47ba0e"; // Seems to be no longer used.
private static final String[] GOOGLE_PRIMARY_KEYS = {GOOGLE_PLATFORM_KEY, GOOGLE_PLATFORM_KEY_2, GOOGLE_APP_KEY};
@Deprecated
public static boolean isGooglePackage(@NonNull Context context, @Nullable String packageName) {
if (packageName == null) return false;
return new ExtendedPackageInfo(context, packageName).isGoogleOrPlatformPackage();
}
/**
* @deprecated Extended access is a deprecated concept
*/
@Deprecated
public static boolean callerHasExtendedAccessPermission(@NonNull Context context) {
return context.checkCallingPermission("org.microg.gms.EXTENDED_ACCESS") == PackageManager.PERMISSION_GRANTED;
}
public static void assertGooglePackagePermission(@NonNull Context context, GooglePackagePermission permission) {
try {
if (!callerHasGooglePackagePermission(context, permission))
throw new SecurityException("Access denied, missing google package permission for " + permission.name());
} catch (SecurityException e) {
Log.w("ExtendedAccess", e);
throw e;
}
}
public static boolean callerHasGooglePackagePermission(@NonNull Context context, GooglePackagePermission permission) {
for (String packageCandidate : getCallingPackageCandidates(context)) {
if (new ExtendedPackageInfo(context, packageCandidate).hasGooglePackagePermission(permission)) {
return true;
}
}
// TODO: Replace with explicit permission instead of generic "extended access"
if (callerHasExtendedAccessPermission(context)) return true;
return false;
}
public static void checkPackageUid(@NonNull Context context, @NonNull String packageName, int callingUid) {
getAndCheckPackage(context, packageName, callingUid, 0);
}
/**
* @deprecated We should stop using SHA-1 for certificate fingerprints!
*/
@Deprecated
@Nullable
public static String firstSignatureDigest(@NonNull Context context, @Nullable String packageName) {
return firstSignatureDigest(context, packageName, false);
}
/**
* @deprecated We should stop using SHA-1 for certificate fingerprints!
*/
@Deprecated
@Nullable
public static String firstSignatureDigest(@NonNull Context context, @Nullable String packageName, boolean useSigningInfo) {
return firstSignatureDigest(context.getPackageManager(), packageName, useSigningInfo);
}
/**
* @deprecated We should stop using SHA-1 for certificate fingerprints!
*/
@Deprecated
@Nullable
public static String firstSignatureDigest(@NonNull PackageManager packageManager, @Nullable String packageName) {
return firstSignatureDigest(packageManager, packageName, false);
}
/**
* @deprecated We should stop using SHA-1 for certificate fingerprints!
*/
@Deprecated
@Nullable
public static String firstSignatureDigest(@NonNull PackageManager packageManager, String packageName, boolean useSigningInfo) {
return bytesToSumString(firstSignatureDigestBytes(packageManager, packageName, useSigningInfo));
}
/**
* @deprecated We should stop using SHA-1 for certificate fingerprints!
*/
@Deprecated
@Nullable
public static byte[] firstSignatureDigestBytes(@NonNull Context context, @Nullable String packageName) {
return firstSignatureDigestBytes(context.getPackageManager(), packageName);
}
/**
* @deprecated We should stop using SHA-1 for certificate fingerprints!
*/
@Deprecated
@Nullable
public static byte[] firstSignatureDigestBytes(@NonNull PackageManager packageManager, @Nullable String packageName) {
return firstSignatureDigestBytes(packageManager, packageName, false);
}
/**
* @deprecated We should stop using SHA-1 for certificate fingerprints!
*/
@Deprecated
@Nullable
public static byte[] firstSignatureDigestBytes(@NonNull PackageManager packageManager, @Nullable String packageName, boolean useSigningInfo) {
if (packageName == null) return null;
final PackageInfo info;
try {
info = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES | (useSigningInfo && SDK_INT >= 28 ? PackageManager.GET_SIGNING_CERTIFICATES : 0));
} catch (PackageManager.NameNotFoundException e) {
return null;
}
if (info == null) return null;
if (SDK_INT >= 28 && useSigningInfo && info.signingInfo != null) {
if (!info.signingInfo.hasMultipleSigners()) {
for (Signature sig : info.signingInfo.getSigningCertificateHistory()) {
byte[] digest = sha1bytes(sig.toByteArray());
if (digest != null) {
return digest;
}
}
}
}
if (info.signatures != null) {
for (Signature sig : info.signatures) {
byte[] digest = sha1bytes(sig.toByteArray());
if (digest != null) {
return digest;
}
}
}
return null;
}
@Nullable
public static String getCallingPackage(@NonNull Context context) {
int callingUid = Binder.getCallingUid(), callingPid = Binder.getCallingPid();
String packageName = packageFromProcessId(context, callingPid);
if (packageName == null) {
packageName = firstPackageFromUserId(context, callingUid);
}
return packageName;
}
public static String[] getCallingPackageCandidates(@NonNull Context context) {
int callingUid = Binder.getCallingUid(), callingPid = Binder.getCallingPid();
String packageName = packageFromProcessId(context, callingPid);
if (packageName != null) return new String[]{packageName};
String[] candidates = context.getPackageManager().getPackagesForUid(callingUid);
if (candidates == null) return new String[0];
return candidates;
}
@Nullable
public static String getAndCheckCallingPackage(@NonNull Context context, @Nullable String suggestedPackageName) {
return getAndCheckCallingPackage(context, suggestedPackageName, 0);
}
@Nullable
public static String getAndCheckCallingPackageOrImpersonation(@NonNull Context context, @Nullable String suggestedPackageName) {
try {
return getAndCheckCallingPackage(context, suggestedPackageName, 0);
} catch (Exception e) {
if (callerHasGooglePackagePermission(context, GooglePackagePermission.IMPERSONATE)) {
return suggestedPackageName;
}
throw e;
}
}
@Nullable
public static String getAndCheckCallingPackage(@NonNull Context context, int suggestedCallerUid) {
return getAndCheckCallingPackage(context, null, suggestedCallerUid);
}
@Nullable
public static String getAndCheckCallingPackage(@NonNull Context context, @Nullable String suggestedPackageName, int suggestedCallerUid) {
return getAndCheckCallingPackage(context, suggestedPackageName, suggestedCallerUid, 0);
}
@Nullable
public static String getAndCheckCallingPackage(@NonNull Context context, @Nullable String suggestedPackageName, int suggestedCallerUid, int suggestedCallerPid) {
int callingUid = Binder.getCallingUid(), callingPid = Binder.getCallingPid();
if (suggestedCallerUid > 0 && suggestedCallerUid != callingUid) {
throw new SecurityException("suggested UID [" + suggestedCallerUid + "] and real calling UID [" + callingUid + "] mismatch!");
}
if (suggestedCallerPid > 0 && suggestedCallerPid != callingPid) {
throw new SecurityException("suggested PID [" + suggestedCallerPid + "] and real calling PID [" + callingPid + "] mismatch!");
}
return getAndCheckPackage(context, suggestedPackageName, callingUid, callingPid);
}
@Nullable
public static String getAndCheckPackage(Context context, String suggestedPackageName, int callingUid) {
return getAndCheckPackage(context, suggestedPackageName, callingUid, 0);
}
@Nullable
public static String getAndCheckPackage(@NonNull Context context, @Nullable String suggestedPackageName, int callingUid, int callingPid) {
String packageName = packageFromProcessId(context, callingPid);
if (packageName == null) {
String[] packagesForUid = context.getPackageManager().getPackagesForUid(callingUid);
if (packagesForUid != null && packagesForUid.length != 0) {
if (packagesForUid.length == 1) {
packageName = packagesForUid[0];
} else if (Arrays.asList(packagesForUid).contains(suggestedPackageName)) {
packageName = suggestedPackageName;
} else {
packageName = packagesForUid[0];
}
}
}
if (packageName != null && suggestedPackageName != null && !packageName.equals(suggestedPackageName)) {
throw new SecurityException("UID [" + callingUid + "] is not related to packageName [" + suggestedPackageName + "] (seems to be " + packageName + ")");
}
return packageName;
}
@Nullable
@Deprecated
public static String packageFromProcessId(@NonNull Context context, int pid) {
ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (manager == null) return null;
if (pid <= 0) return null;
List<ActivityManager.RunningAppProcessInfo> runningAppProcesses = manager.getRunningAppProcesses();
if (runningAppProcesses != null) {
for (ActivityManager.RunningAppProcessInfo processInfo : runningAppProcesses) {
if (processInfo.pid == pid && processInfo.pkgList.length == 1) {
return processInfo.pkgList[0];
}
}
}
return null;
}
@Nullable
public static String firstPackageFromUserId(@NonNull Context context, int uid) {
String[] packagesForUid = context.getPackageManager().getPackagesForUid(uid);
if (packagesForUid != null && packagesForUid.length != 0) {
return packagesForUid[0];
}
return null;
}
@SuppressWarnings("deprecation")
public static String packageFromPendingIntent(@Nullable PendingIntent pi) {
if (pi == null) return null;
if (SDK_INT < 17) {
return pi.getTargetPackage();
} else {
return pi.getCreatorPackage();
}
}
public static String getProcessName() {
if (android.os.Build.VERSION.SDK_INT >= 28)
return Application.getProcessName();
try {
Class<?> activityThread = Class.forName("android.app.ActivityThread");
String methodName = android.os.Build.VERSION.SDK_INT >= 18 ? "currentProcessName" : "currentPackageName";
Method getProcessName = activityThread.getDeclaredMethod(methodName);
return (String) getProcessName.invoke(null);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static boolean isPersistentProcess() {
String processName = getProcessName();
if (processName == null) {
Log.w("GmsPackageUtils", "Can't determine process name of current process");
return false;
}
return processName.endsWith(":persistent");
}
public static boolean isMainProcess(Context context) {
String processName = getProcessName();
if (processName == null) {
Log.w("GmsPackageUtils", "Can't determine process name of current process");
return false;
}
return processName.equals(context.getPackageName());
}
public static void warnIfNotPersistentProcess(Class<?> clazz) {
if (!isPersistentProcess()) {
Log.w("GmsPackageUtils", clazz.getSimpleName() + " initialized outside persistent process", new RuntimeException());
}
}
public static void warnIfNotMainProcess(Context context, Class<?> clazz) {
if (!isMainProcess(context)) {
Log.w("GmsPackageUtils", clazz.getSimpleName() + " initialized outside main process", new RuntimeException());
}
}
/**
* @deprecated We should stop using SHA-1 for certificate fingerprints!
*/
@Deprecated
public static String sha1sum(byte[] bytes) {
return bytesToSumString(sha1bytes(bytes));
}
@Nullable
private static String bytesToSumString(@Nullable byte[] bytes) {
if (bytes == null) return null;
StringBuilder sb = new StringBuilder(2 * bytes.length);
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
/**
* @deprecated We should stop using SHA-1 for certificate fingerprints!
*/
@Deprecated
public static byte[] sha1bytes(byte[] bytes) {
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA1");
} catch (final NoSuchAlgorithmException e) {
return null;
}
if (md != null) {
return md.digest(bytes);
}
return null;
}
@Deprecated
public static int versionCode(Context context, String packageName) {
return new ExtendedPackageInfo(context, packageName).getShortVersionCode();
}
@Deprecated
public static String versionName(Context context, String packageName) {
return new ExtendedPackageInfo(context, packageName).getVersionName();
}
@Deprecated
public static int targetSdkVersion(Context context, String packageName) {
return new ExtendedPackageInfo(context, packageName).getTargetSdkVersion();
}
}

View file

@ -0,0 +1,35 @@
/*
* 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.common;
import java.util.Random;
public class PhoneInfo {
public String cellOperator = "26207";
public String roaming = "mobile-notroaming";
public String simOperator = "26207";
public String imsi = randomImsi();
private String randomImsi() {
Random random = new Random();
StringBuilder sb = new StringBuilder(simOperator);
while (sb.length() < 15) {
sb.append(random.nextInt(10));
}
return sb.toString();
}
}

View file

@ -0,0 +1,138 @@
/*
* Copyright (C) 2013-2019 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.common;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.ResolveInfo;
import android.os.IBinder;
import android.os.IInterface;
import android.util.Log;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
public class RemoteListenerProxy<T extends IInterface> implements ServiceConnection, InvocationHandler {
private static final String TAG = "GmsRemoteListener";
private final Context context;
private final Intent searchIntent;
private final String bindAction;
private IBinder remote;
private boolean connecting;
private List<Runnable> waiting = new ArrayList<Runnable>();
private Class<T> tClass;
public static <T extends IInterface> T get(Context context, Intent intent, Class<T> tClass, String bindAction) {
return (T) Proxy.newProxyInstance(tClass.getClassLoader(), new Class[]{tClass},
new RemoteListenerProxy<T>(context, intent, tClass, bindAction));
}
private RemoteListenerProxy(Context context, Intent intent, Class<T> tClass, String bindAction) {
this.context = context;
this.searchIntent = intent;
this.tClass = tClass;
this.bindAction = bindAction;
}
private boolean connect() {
synchronized (this) {
if (!connecting) {
try {
ResolveInfo resolveInfo = context.getPackageManager().resolveService(searchIntent, 0);
if (resolveInfo != null) {
Intent intent = new Intent(bindAction);
intent.setPackage(resolveInfo.serviceInfo.packageName);
intent.setClassName(resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name);
connecting = context.bindService(intent, this, Context.BIND_AUTO_CREATE);
if (!connecting) Log.d(TAG, "Could not connect to: " + intent);
return connecting;
}
return false;
} catch (Exception e) {
Log.w(TAG, e);
}
}
return true;
}
}
private void runOncePossible(Runnable runnable) {
synchronized (this) {
if (remote == null) {
waiting.add(runnable);
} else {
runnable.run();
}
}
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (this) {
remote = service;
if (!waiting.isEmpty()) {
try {
for (Runnable runnable : waiting) {
runnable.run();
}
} catch (Exception e) {
}
waiting.clear();
try {
context.unbindService(RemoteListenerProxy.this);
} catch (Exception e) {
}
connecting = false;
remote = null;
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
synchronized (this) {
remote = null;
}
}
@Override
public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable {
if (method.getDeclaringClass().equals(tClass)) {
runOncePossible(new Runnable() {
@Override
public void run() {
try {
Object asInterface = Class.forName(tClass.getName() + "$Stub").getMethod("asInterface", IBinder.class).invoke(null, remote);
method.invoke(asInterface, args);
} catch (Exception e) {
Log.w(TAG, e);
}
}
});
connect();
return null;
} else if (method.getDeclaringClass().equals(Object.class)) {
return method.invoke(this, args);
}
return null;
}
}

View file

@ -0,0 +1,68 @@
/*
* 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.common;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
public class Utils {
public static Locale getLocale(Context context) {
return Locale.getDefault(); // TODO
}
public static DeviceIdentifier getDeviceIdentifier(Context context) {
return new DeviceIdentifier();
}
public static PhoneInfo getPhoneInfo(Context context) {
return new PhoneInfo();
}
public static boolean hasSelfPermissionOrNotify(Context context, String permission) {
if (context.checkCallingOrSelfPermission(permission) != PERMISSION_GRANTED) {
Log.w("GmsUtils", "Lacking permission to " + permission + " for pid:" + android.os.Process.myPid() + " uid:" + android.os.Process.myUid());
try {
//TODO: Toast.makeText(context, context.getString(R.string.lacking_permission_toast, permission), Toast.LENGTH_SHORT).show();
} catch (RuntimeException e) {
}
return false;
}
return true;
}
public static byte[] readStreamToEnd(final InputStream is) throws IOException {
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
if (is != null) {
final byte[] buff = new byte[1024];
int read;
do {
bos.write(buff, 0, (read = is.read(buff)) < 0 ? 0 : read);
} while (read >= 0);
is.close();
}
return bos.toByteArray();
}
}

View file

@ -0,0 +1,43 @@
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.getContentUri(context), arrayOf(Auth.TRUST_GOOGLE)) { c ->
c.getInt(0) != 0
}
}
@JvmStatic
fun isAuthVisible(context: Context): Boolean {
return SettingsContract.getSettings(context, Auth.getContentUri(context), arrayOf(Auth.VISIBLE)) { c ->
c.getInt(0) != 0
}
}
@JvmStatic
fun shouldIncludeAndroidId(context: Context): Boolean {
return SettingsContract.getSettings(context, Auth.getContentUri(context), arrayOf(Auth.INCLUDE_ANDROID_ID)) { c ->
c.getInt(0) != 0
}
}
@JvmStatic
fun shouldStripDeviceName(context: Context): Boolean {
return SettingsContract.getSettings(context, Auth.getContentUri(context), arrayOf(Auth.STRIP_DEVICE_NAME)) { c ->
c.getInt(0) != 0
}
}
@JvmStatic
fun shouldReceiveTwoStepVerification(context: Context): Boolean {
return SettingsContract.getSettings(context, Auth.getContentUri(context), arrayOf(Auth.TWO_STEP_VERIFICATION)) { c ->
c.getInt(0) != 0
}
}
}

View file

@ -0,0 +1,77 @@
/*
* SPDX-FileCopyrightText: 2025 e foundation
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.checkin
import android.content.Context
import org.microg.gms.settings.SettingsContract
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 ?: SettingsContract.CheckIn.INITIAL_DIGEST,
versionInfo = r.versionInfo ?: "",
deviceDataVersionInfo = r.deviceDataVersionInfo ?: "",
)
companion object {
@JvmStatic
fun read(context: Context): LastCheckinInfo {
val projection = arrayOf(
SettingsContract.CheckIn.ANDROID_ID,
SettingsContract.CheckIn.DIGEST,
SettingsContract.CheckIn.LAST_CHECK_IN,
SettingsContract.CheckIn.SECURITY_TOKEN,
SettingsContract.CheckIn.VERSION_INFO,
SettingsContract.CheckIn.DEVICE_DATA_VERSION_INFO,
)
return SettingsContract.getSettings(
context,
SettingsContract.CheckIn.getContentUri(context),
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, SettingsContract.CheckIn.getContentUri(context)) {
put(SettingsContract.CheckIn.ANDROID_ID, 0L)
put(SettingsContract.CheckIn.DIGEST, SettingsContract.CheckIn.INITIAL_DIGEST)
put(SettingsContract.CheckIn.LAST_CHECK_IN, 0L)
put(SettingsContract.CheckIn.SECURITY_TOKEN, 0L)
put(SettingsContract.CheckIn.VERSION_INFO, "")
put(SettingsContract.CheckIn.DEVICE_DATA_VERSION_INFO, "")
}
}
fun write(context: Context) =
SettingsContract.setSettings(context, SettingsContract.CheckIn.getContentUri(context)) {
put(SettingsContract.CheckIn.ANDROID_ID, androidId)
put(SettingsContract.CheckIn.DIGEST, digest)
put(SettingsContract.CheckIn.LAST_CHECK_IN, lastCheckin)
put(SettingsContract.CheckIn.SECURITY_TOKEN, securityToken)
put(SettingsContract.CheckIn.VERSION_INFO, versionInfo)
put(SettingsContract.CheckIn.DEVICE_DATA_VERSION_INFO, deviceDataVersionInfo)
}
}

View file

@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: 2025 e foundation
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.common
import org.microg.gms.checkin.DeviceConfig
fun DeviceConfiguration.asProto(): DeviceConfig = DeviceConfig(
availableFeature = availableFeatures,
densityDpi = densityDpi,
glEsVersion = glEsVersion,
glExtension = glExtensions,
hasFiveWayNavigation = hasFiveWayNavigation,
hasHardKeyboard = hasHardKeyboard,
heightPixels = heightPixels,
keyboardType = keyboardType,
locale = locales,
nativePlatform = nativePlatforms,
navigation = navigation,
screenLayout = screenLayout,
sharedLibrary = sharedLibraries,
touchScreen = touchScreen,
widthPixels = widthPixels
)

View file

@ -0,0 +1,222 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.common
import com.google.android.gms.common.internal.CertData
import org.microg.gms.common.GooglePackagePermission.*
import org.microg.gms.utils.digest
import org.microg.gms.utils.toHexString
enum class GooglePackagePermission {
ACCOUNT, // Find accounts
AD_ID, // Advertising ID
APP_CERT, // Receive certificate confirming valid app installation (incl. Spatula)
AUTH, // Sign in to Google account without user interface confirmation
CREDENTIALS, // Access to credentials
GAMES, // Google Play Games first party access
IMPERSONATE, // Allow to act as another package
OWNER, // Details about own accounts (name, email, photo)
PEOPLE, // Details about contacts
REPORTING, // Access reporting service
SAFETYNET, // Access SafetyNet UUID
}
// These are SHA-256 hashes of the Google privileged signing certificates
private val KNOWN_GOOGLE_PRIVILEGED_CERT_HASHES = listOf(
"f0fd6c5b410f25cb25c3b53346c8972fae30f8ee7411df910480ad6b2d60db83",
"7ce83c1b71f3d572fed04c8d40c5cb10ff75e6d87d9df6fbd53f0468c2905053",
)
// These are the permissions that we grant to apps signed with a Google
// privileged platform signing certificate. Those could be in the same
// shared UID on regular Play Services and thus have full access by
// design, as they could even directly access all private details in GMS.
private val PERMISSIONS_PRIVILEGED = GooglePackagePermission.entries.toSet()
// These are SHA-256 hashes of signing certificates used by official Google apps
// Signing certificates that are only used for a small number of apps are likely not
// official Google apps, but either acquisitions or independent teams / projects
// within Google. We don't put them here, but via KNOWN_GOOGLE_PACKAGES.
private val KNOWN_GOOGLE_APP_CERT_HASHES = listOf(
"3d7a1223019aa39d9ea0e3436ab7c0896bfb4fb679f4de5fe7c23f326c8f994a"
)
// This is a subset of permissions that we grant to apps signed with an official
// Google apps certificate. Note that this has lower priority than the
// KNOWN_GOOGLE_PACKAGES list, so if any app needs more permissions than this,
// this can be handled through KNOWN_GOOGLE_PACKAGES.
private val PERMISSIONS_APP = setOf(ACCOUNT, APP_CERT, AUTH, OWNER, PEOPLE, REPORTING, SAFETYNET)
private const val SHA1 = "SHA1"
private const val SHA256 = "SHA-256"
data class PackageAndCertHash(val packageName: String, val algorithm: String, val certHash: String)
private val KNOWN_GOOGLE_PACKAGES = mapOf(
// Legacy set
// These include all previously KNOWN_GOOGLE_PACKAGES and grant them all google package permissions
// Those should be replaced by new entries that
// - use SHA-256 instead of SHA-1
// - has more accurate permission set (in most cases, ACCOUNT+AUTH+OWNER is sufficient)
Pair(
PackageAndCertHash("com.google.android.apps.classroom", SHA1, "46f6c8987311e131f4f558d8e0ae145bebab6da3"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.android.apps.inbox", SHA1, "aa87ce1260c008d801197bb4ecea4ab8929da246"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.android.apps.playconsole", SHA1, "d6c35e55b481aefddd74152ca7254332739a81d6"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.android.apps.travel.onthego", SHA1, "0cbe08032217d45e61c0bc72f294395ee9ecb5d5"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.android.apps.tycho", SHA1, "01b844184e360686aa98b48eb16e05c76d4a72ad"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.android.contacts", SHA1, "ee3e2b5d95365c5a1ccc2d8dfe48d94eb33b3ebe"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.android.wearable.app", SHA1, "a197f9212f2fed64f0ff9c2a4edf24b9c8801c8c"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.android.apps.youtube.music", SHA1, "afb0fed5eeaebdd86f56a97742f4b6b33ef59875"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.android.vr.home", SHA1, "fc1edc68f7e3e4963c998e95fc38f3de8d1bfc96"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.vr.cyclops", SHA1, "188c5ca3863fa121216157a5baa80755ceda70ab"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.waze", SHA1, "35b438fe1bc69d975dc8702dc16ab69ebf65f26f"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.android.apps.wellbeing", SHA1, "4ebdd02380f1fa0b6741491f0af35625dba76e9f"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.android.apps.village.boond", SHA1, "48e7985b8f901df335b5d5223579c81618431c7b"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.android.apps.subscriptions.red", SHA1, "de8304ace744ae4c4e05887a27a790815e610ff0"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.android.apps.meetings", SHA1, "47a6936b733dbdb45d71997fbe1d610eca36b8bf"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.android.apps.nbu.paisa.user", SHA1, "80df78bb700f9172bc671779b017ddefefcbf552"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.android.apps.dynamite", SHA1, "519c5a17a60596e6fe5933b9cb4285e7b0e5eb7b"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.android.projection.gearhead", SHA1, "9ca91f9e704d630ef67a23f52bf1577a92b9ca5d"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.stadia.android", SHA1, "133aad3b3d3b580e286573c37f20549f9d3d1cce"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.android.apps.kids.familylink", SHA1, "88652b8464743e5ce80da0d4b890d13f9b1873df"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.android.apps.walletnfcrel", SHA1, "82759e2db43f9ccbafce313bc674f35748fabd7a"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.android.apps.recorder", SHA1, "394d84cd2cf89d3453702c663f98ec6554afc3cd"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.android.apps.messaging", SHA1, "0980a12be993528c19107bc21ad811478c63cefc"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.android.apps.tachyon", SHA1, "a0bc09af527b6397c7a9ef171d6cf76f757becc3"),
PERMISSIONS_PRIVILEGED
),
Pair(
PackageAndCertHash("com.google.android.apps.access.wifi.consumer", SHA1,"d850379540d68fbec82a742ab6a8321a3f9a4c7c"),
PERMISSIONS_PRIVILEGED
),
// Google Jamboard
Pair(
PackageAndCertHash("com.google.android.apps.jam", SHA256, "9db7ff389ab6a30d5f5c92a8629ff0baa93fa8430f0503c04d72640a1cf323f5"),
setOf(ACCOUNT, AUTH, OWNER)
),
// Fitbit
Pair(
PackageAndCertHash("com.fitbit.FitbitMobile", SHA256, "fa6a198803aac1939fed6bab9295e5184c00966bf912f8c5faff26576cc770ff"),
setOf(ACCOUNT, AUTH, OWNER)
),
// Google Tasks
Pair(
PackageAndCertHash("com.google.android.apps.tasks", SHA256, "99f6cc5308e6f3318a3bf168bf106d5b5defe2b4b9c561e5ddd7924a7a2ba1e2"),
setOf(ACCOUNT, AUTH, OWNER)
),
// Google familylink
Pair(
PackageAndCertHash("com.google.android.apps.kids.familylink", SHA256, "6b58bb84c1c6d081d950448ff5c051a34769d7fd8d415452c86efeb808716c0e"),
setOf(ACCOUNT, AUTH, OWNER)
),
// Google Kids home
Pair(
PackageAndCertHash("com.google.android.apps.kids.home", SHA256, "8f7bd4c5c0273a1a0dd6b3bfa8cc8e9f980a25108adcfd7be9962e8ae9feeb6f"),
setOf(ACCOUNT, AUTH, OWNER)
),
// Google GFiber
Pair(
PackageAndCertHash("com.google.android.apps.fiber.myfiber", SHA256, "4a853c50adda4406495652fe78f32252757c8dd761f3601a7b2e0df86291429d"),
setOf(ACCOUNT, AUTH, OWNER)
),
// Google NotebookLM
Pair(
PackageAndCertHash("com.google.android.apps.labs.language.tailwind", SHA256, "ba49176908275f83be9ae1034968f0b18e65177a64e5a40b3a621f148dfb6fa2"),
setOf(ACCOUNT, AUTH, OWNER)
),
)
fun isGooglePackage(pkg: PackageAndCertHash): Boolean {
if (pkg.algorithm == SHA256 && pkg.certHash in KNOWN_GOOGLE_PRIVILEGED_CERT_HASHES) return true
if (pkg.algorithm == SHA256 && pkg.certHash in KNOWN_GOOGLE_APP_CERT_HASHES) return true
return KNOWN_GOOGLE_PACKAGES.containsKey(pkg)
}
fun getGooglePackagePermissions(pkg: PackageAndCertHash): Set<GooglePackagePermission> {
if (KNOWN_GOOGLE_PACKAGES.containsKey(pkg)) return KNOWN_GOOGLE_PACKAGES[pkg].orEmpty()
if (pkg.algorithm == SHA256 && pkg.certHash in KNOWN_GOOGLE_PRIVILEGED_CERT_HASHES) return PERMISSIONS_PRIVILEGED
if (pkg.algorithm == SHA256 && pkg.certHash in KNOWN_GOOGLE_APP_CERT_HASHES) return PERMISSIONS_APP
return emptySet()
}
fun hasGooglePackagePermission(pkg: PackageAndCertHash, permission: GooglePackagePermission) = getGooglePackagePermissions(pkg).contains(permission)
fun isGooglePackage(packageName: String, certificate: CertData): Boolean =
listOf(SHA1, SHA256).any { isGooglePackage(PackageAndCertHash(packageName, it, certificate.digest(it).toHexString())) }

View file

@ -0,0 +1,90 @@
/*
* SPDX-FileCopyrightText: 2025 e foundation
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.crossprofile
import android.app.Activity
import android.content.Intent
import android.content.pm.CrossProfileApps
import android.os.Bundle
import android.os.UserManager
import android.util.Log
import androidx.annotation.RequiresApi
import org.microg.gms.settings.SettingsContract.CROSS_PROFILE_PERMISSION
import org.microg.gms.settings.SettingsContract.CROSS_PROFILE_SHARED_PREFERENCES_NAME
import androidx.core.content.edit
/**
* Two-step process:
* 1. request to hear back from `CrossProfileRequestActivity`
* 2. receive resulting URI as intent data
*
* This dance so complicated because Android platform does not offer better APIs that only need
* `INTERACT_ACROSS_PROFILES`, an appops permission (and not `INTERACT_ACROSS_USERS`, a
* privileged|system permission).
*/
@RequiresApi(30)
class CrossProfileRequestActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Check that we are work profile
val userManager = getSystemService(UserManager::class.java)
if (!userManager.isManagedProfile) {
Log.w(CrossProfileSendActivity.TAG, "I was asked to send a cross-profile request, but I am not on a work profile!")
finish()
return
}
val crossProfileApps = getSystemService(CrossProfileApps::class.java)
val targetProfiles = crossProfileApps.targetUserProfiles
if (!crossProfileApps.canInteractAcrossProfiles() || targetProfiles.isEmpty()) {
Log.w(
TAG, "I am supposed to send a cross-profile request, but the prerequisites are not met: " +
"can interact = ${crossProfileApps.canInteractAcrossProfiles()}, " +
"#targetProfiles = ${targetProfiles.size}")
finish()
return
}
val intent = Intent(this, CrossProfileSendActivity::class.java)
Log.d(TAG, "asking for cross-profile URI")
crossProfileApps.startActivity(
intent,
targetProfiles.first(),
// if this parameter is provided, it works like `startActivityForResult` (with requestCode 0)
this
)
// finish only after receiving result
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
Log.d(TAG, data?.data.toString())
val uri = data?.data
if (uri == null) {
Log.w(TAG, "expected to receive data, but intent did not contain any.")
finish()
return
}
contentResolver.takePersistableUriPermission(uri, 0)
val preferences = getSharedPreferences(CROSS_PROFILE_SHARED_PREFERENCES_NAME, MODE_PRIVATE)
Log.i(TAG, "storing work URI")
preferences.edit { putString(CROSS_PROFILE_PERMISSION, uri.toString()) }
finish()
}
companion object {
const val TAG = "GmsCrossProfileRequest"
}
}

View file

@ -0,0 +1,57 @@
/*
* SPDX-FileCopyrightText: 2025 e foundation
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.crossprofile
import android.app.Activity
import android.content.Intent
import android.content.Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
import android.content.pm.CrossProfileApps
import android.os.Bundle
import android.os.UserManager
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.net.toUri
import org.microg.gms.settings.SettingsContract.getAuthority
@RequiresApi(30)
class CrossProfileSendActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Check that we are primary profile
val userManager = getSystemService(UserManager::class.java)
if (userManager.isManagedProfile) {
Log.w(TAG, "Cross-profile send request was received on work profile!")
finish()
return
}
// Check prerequisites
val crossProfileApps = getSystemService(CrossProfileApps::class.java)
val targetProfiles = crossProfileApps.targetUserProfiles
if (!crossProfileApps.canInteractAcrossProfiles() || targetProfiles.isEmpty()) {
Log.w(
TAG, "received cross-profile request, but I believe I cannot answer, as prerequisites are not met: " +
"can interact = ${crossProfileApps.canInteractAcrossProfiles()}, " +
"#targetProfiles = ${targetProfiles.size}. Note that this is expected during initial setup of a work profile.")
}
// Respond
Log.d(TAG, "responding to cross-profile request")
setResult(1, Intent().apply {
setData("content://${getAuthority(this@CrossProfileSendActivity)}".toUri())
addFlags(FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
})
finish()
}
companion object {
const val TAG = "GmsCrossProfileSend"
}
}

View file

@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: 2025 e foundation
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.crossprofile
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build.VERSION.SDK_INT
import android.os.UserManager
import android.util.Log
class UserInitReceiver : BroadcastReceiver() {
@SuppressLint("UnsafeProtectedBroadcastReceiver") // exported="false"
override fun onReceive(context: Context, intent: Intent?) {
// Check that we are work profile
if (SDK_INT >= 30) {
val userManager = context.getSystemService(UserManager::class.java)
if (userManager.isManagedProfile) {
Log.d(TAG, "A new managed profile is being initialized; telling `CrossProfileRequestActivity` to request access to main profile's data.")
// CrossProfileActivity will check whether permissions are present
context.startActivity(
Intent(context, CrossProfileRequestActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
)
} else {
Log.d(TAG, "A new user is being initialized, but it is not a managed profile. Not connecting data")
}
}
}
companion object {
const val TAG = "GmsUserInit"
}
}

View file

@ -0,0 +1,104 @@
/*
* SPDX-FileCopyrightText: 2021 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.profile
import android.annotation.TargetApi
object Build {
@JvmField
var BOARD: String? = null
@JvmField
var BOOTLOADER: String? = null
@JvmField
var BRAND: String? = null
@JvmField
var CPU_ABI: String? = null
@JvmField
var CPU_ABI2: String? = null
@JvmField
@TargetApi(21)
var SUPPORTED_ABIS: Array<String> = emptyArray()
@JvmField
var DEVICE: String? = null
@JvmField
var DISPLAY: String? = null
@JvmField
var FINGERPRINT: String? = null
@JvmField
var HARDWARE: String? = null
@JvmField
var HOST: String? = null
@JvmField
var ID: String? = null
@JvmField
var MANUFACTURER: String? = null
@JvmField
var MODEL: String? = null
@JvmField
var PRODUCT: String? = null
@JvmField
var RADIO: String? = null
@JvmField
var SERIAL: String? = null
@JvmField
var TAGS: String? = null
@JvmField
var TIME: Long = 0L
@JvmField
var TYPE: String? = null
@JvmField
var USER: String? = null
object VERSION {
@JvmField
var CODENAME: String? = null
@JvmField
var INCREMENTAL: String? = null
@JvmField
var RELEASE: String? = null
@JvmField
var SDK: String? = null
@JvmField
var SDK_INT: Int = 0
@JvmField
var SECURITY_PATCH: String? = null
@JvmField
var DEVICE_INITIAL_SDK_INT: Int = 0
}
fun generateWebViewUserAgentString(original: String): String {
if (!original.startsWith("Mozilla/5.0 (")) return original
val closeParen: Int = original.indexOf(')')
return "Mozilla/5.0 (Linux; Android ${VERSION.RELEASE}; $MODEL Build/$ID; wv)${original.substring(closeParen + 1)}"
}
}

View file

@ -0,0 +1,408 @@
/*
* SPDX-FileCopyrightText: 2021, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.profile
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.XmlResourceParser
import android.net.Uri
import android.os.Bundle
import android.util.Log
import org.microg.gms.settings.SettingsContract
import org.microg.gms.settings.SettingsContract.Profile
import org.microg.gms.utils.FileXmlResourceParser
import org.xmlpull.v1.XmlPullParser
import java.io.File
import java.util.*
import kotlin.random.Random
object ProfileManager {
private const val TAG = "ProfileManager"
const val META_DATA_KEY_SOURCE_PACKAGE = "org.microg.gms.profile:source-package"
const val PROFILE_REAL = "real"
const val PROFILE_AUTO = "auto"
const val PROFILE_NATIVE = "native"
const val PROFILE_USER = "user"
const val PROFILE_SYSTEM = "system"
const val PROFILE_REMOTE = "remote"
private var activeProfile: String? = null
private fun getUserProfileFile(context: Context): File = File(context.filesDir, "device_profile.xml")
private fun getSystemProfileFile(context: Context): File = File("/system/etc/microg_device_profile.xml")
private fun getProfileResId(context: Context, profile: String) = context.resources.getIdentifier("${context.packageName}:xml/profile_$profile".toLowerCase(Locale.US), null, null)
fun getConfiguredProfile(context: Context): String = SettingsContract.getSettings(context, Profile.getContentUri(context), arrayOf(Profile.PROFILE)) { it.getString(0) } ?: PROFILE_AUTO
fun getAutoProfile(context: Context): String {
if (hasProfile(context, PROFILE_SYSTEM) && isAutoProfile(context, PROFILE_SYSTEM)) return PROFILE_SYSTEM
val profile = "${android.os.Build.PRODUCT}_${android.os.Build.VERSION.SDK_INT}"
if (hasProfile(context, profile) && isAutoProfile(context, profile)) return profile
return PROFILE_NATIVE
}
fun hasProfile(context: Context, profile: String): Boolean = when (profile) {
PROFILE_AUTO -> hasProfile(context, getAutoProfile(context))
PROFILE_NATIVE, PROFILE_REAL -> true
PROFILE_USER -> getUserProfileFile(context).exists()
PROFILE_SYSTEM -> getSystemProfileFile(context).exists()
else -> getProfileResId(context, profile) != 0
}
private fun getProfileXml(context: Context, profile: String): XmlResourceParser? = kotlin.runCatching {
when (profile) {
PROFILE_AUTO -> getProfileXml(context, getAutoProfile(context))
PROFILE_NATIVE, PROFILE_REAL -> null
PROFILE_USER -> FileXmlResourceParser(getUserProfileFile(context))
PROFILE_SYSTEM -> FileXmlResourceParser(getSystemProfileFile(context))
else -> {
val profileResId = getProfileResId(context, profile)
if (profileResId == 0) return@runCatching null
context.resources.getXml(profileResId)
}
}
}.getOrNull()
fun isAutoProfile(context: Context, profile: String): Boolean = kotlin.runCatching {
when (profile) {
PROFILE_AUTO -> false
PROFILE_REAL -> false
PROFILE_NATIVE -> true
else -> {
val parser = getProfileXml(context, profile)
if (parser != null) {
try {
var next = parser.next()
while (next != XmlPullParser.END_DOCUMENT) {
when (next) {
XmlPullParser.START_TAG -> when (parser.name) {
"profile" -> {
return@runCatching parser.getAttributeBooleanValue(null, "auto", false)
}
}
}
next = parser.next()
}
} finally {
parser.close()
}
false
} else {
false
}
}
}
}.getOrDefault(false)
fun getActiveProfileData(context: Context): Map<String, String> =
getProfileData(context, getProfile(context), getRealData())
private fun getProfileData(context: Context, profile: String, realData: Map<String, String>): Map<String, String> {
try {
if (profile in listOf(PROFILE_REAL, PROFILE_NATIVE)) return realData
if (profile != PROFILE_USER && getProfileResId(context, profile) == 0) return realData
val resultData = mutableMapOf<String, String>()
resultData.putAll(realData)
val parser = getProfileXml(context, profile)
if (parser != null) {
try {
var next = parser.next()
while (next != XmlPullParser.END_DOCUMENT) {
when (next) {
XmlPullParser.START_TAG -> when (parser.name) {
"data" -> {
val key = parser.getAttributeValue(null, "key")
val value = parser.getAttributeValue(null, "value")
resultData[key] = value
Log.d(TAG, "Overwrite from profile: $key = $value")
}
}
}
next = parser.next()
}
} finally {
parser.close()
}
}
return resultData
} catch (e: Exception) {
Log.w(TAG, e)
return realData
}
}
private fun getRemoteProfileData(context: Context, packageName: String): Map<String, String> {
val data = mutableMapOf<String, String>()
val cursor = context.contentResolver.query(Uri.parse("content://${packageName}.microg.profile"), null, null, null, null)
cursor?.use {
while (cursor.moveToNext()) {
data[cursor.getString(0)] = cursor.getString(1)
}
}
return data
}
private fun getProfile(context: Context) = getConfiguredProfile(context).let { if (it != PROFILE_AUTO) it else getAutoProfile(context) }
private fun getSerialFromSettings(context: Context): String? = SettingsContract.getSettings(context, Profile.getContentUri(context), arrayOf(Profile.SERIAL)) { it.getString(0) }
private fun saveSerial(context: Context, serial: String) = SettingsContract.setSettings(context, Profile.getContentUri(context)) { put(Profile.SERIAL, serial) }
private fun randomSerial(template: String, prefixLength: Int = (template.length / 2).coerceAtMost(6)): String {
val serial = StringBuilder()
template.forEachIndexed { index, c ->
serial.append(when {
index < prefixLength -> c
c.isDigit() -> '0' + Random.nextInt(10)
c.isLowerCase() && c <= 'f' -> 'a' + Random.nextInt(6)
c.isLowerCase() -> 'a' + Random.nextInt(26)
c.isUpperCase() && c <= 'F' -> 'A' + Random.nextInt(6)
c.isUpperCase() -> 'A' + Random.nextInt(26)
else -> c
})
}
return serial.toString()
}
@SuppressLint("MissingPermission")
private fun getProfileSerialTemplate(context: Context, profile: String): String {
// Native
if (profile in listOf(PROFILE_REAL, PROFILE_NATIVE)) {
var candidate = try {
if (android.os.Build.VERSION.SDK_INT >= 26) {
android.os.Build.getSerial()
} else {
android.os.Build.SERIAL
}
} catch (e: Exception) {
android.os.Build.SERIAL
}
if (candidate != android.os.Build.UNKNOWN) return candidate
}
// From profile
try {
val parser = getProfileXml(context, profile)
if (parser != null) {
try {
var next = parser.next()
while (next != XmlPullParser.END_DOCUMENT) {
when (next) {
XmlPullParser.START_TAG -> when (parser.name) {
"serial" -> return parser.getAttributeValue(null, "template")
}
}
next = parser.next()
}
} finally {
parser.close()
}
}
} catch (e: Exception) {
Log.w(TAG, e)
}
// Fallback
return randomSerial("008741A0B2C4D6E8")
}
@SuppressLint("MissingPermission")
fun getSerial(context: Context, profile: String = getProfile(context), local: Boolean = false): String {
if (!local) getSerialFromSettings(context)?.let { return it }
val serialTemplate = getProfileSerialTemplate(context, profile)
val serial = when {
profile == PROFILE_REAL && serialTemplate != android.os.Build.UNKNOWN -> serialTemplate
else -> randomSerial(serialTemplate)
}
if (!local) saveSerial(context, serial)
return serial
}
@SuppressLint("BlockedPrivateApi")
private fun getRealData(): Map<String, String> = mutableMapOf(
"Build.BOARD" to android.os.Build.BOARD,
"Build.BOOTLOADER" to android.os.Build.BOOTLOADER,
"Build.BRAND" to android.os.Build.BRAND,
"Build.CPU_ABI" to android.os.Build.CPU_ABI,
"Build.CPU_ABI2" to android.os.Build.CPU_ABI2,
"Build.DEVICE" to android.os.Build.DEVICE,
"Build.DISPLAY" to android.os.Build.DISPLAY,
"Build.FINGERPRINT" to android.os.Build.FINGERPRINT,
"Build.HARDWARE" to android.os.Build.HARDWARE,
"Build.HOST" to android.os.Build.HOST,
"Build.ID" to android.os.Build.ID,
"Build.MANUFACTURER" to android.os.Build.MANUFACTURER,
"Build.MODEL" to android.os.Build.MODEL,
"Build.PRODUCT" to android.os.Build.PRODUCT,
"Build.RADIO" to android.os.Build.RADIO,
"Build.SERIAL" to android.os.Build.SERIAL,
"Build.TAGS" to android.os.Build.TAGS,
"Build.TIME" to android.os.Build.TIME.toString(),
"Build.TYPE" to android.os.Build.TYPE,
"Build.USER" to android.os.Build.USER,
"Build.VERSION.CODENAME" to android.os.Build.VERSION.CODENAME,
"Build.VERSION.INCREMENTAL" to android.os.Build.VERSION.INCREMENTAL,
"Build.VERSION.RELEASE" to android.os.Build.VERSION.RELEASE,
"Build.VERSION.SDK" to android.os.Build.VERSION.SDK,
"Build.VERSION.SDK_INT" to android.os.Build.VERSION.SDK_INT.toString()
).apply {
if (android.os.Build.VERSION.SDK_INT >= 21) {
put("Build.SUPPORTED_ABIS", android.os.Build.SUPPORTED_ABIS.joinToString(","))
}
if (android.os.Build.VERSION.SDK_INT >= 23) {
put("Build.VERSION.SECURITY_PATCH", android.os.Build.VERSION.SECURITY_PATCH)
}
try {
val field = android.os.Build.VERSION::class.java.getDeclaredField("DEVICE_INITIAL_SDK_INT")
field.isAccessible = true
put("Build.VERSION.DEVICE_INITIAL_SDK_INT", field.getInt(null).toString())
} catch (ignored: Exception) {
}
}
fun applyProfileData(profileData: Map<String, String>) {
fun applyStringField(key: String, valueSetter: (String) -> Unit) = profileData[key]?.let { valueSetter(it) }
fun applyIntField(key: String, valueSetter: (Int) -> Unit) = profileData[key]?.toIntOrNull()?.let { valueSetter(it) }
fun applyLongField(key: String, valueSetter: (Long) -> Unit) = profileData[key]?.toLongOrNull()?.let { valueSetter(it) }
applyStringField("Build.BOARD") { Build.BOARD = it }
applyStringField("Build.BOOTLOADER") { Build.BOOTLOADER = it }
applyStringField("Build.BRAND") { Build.BRAND = it }
applyStringField("Build.CPU_ABI") { Build.CPU_ABI = it }
applyStringField("Build.CPU_ABI2") { Build.CPU_ABI2 = it }
applyStringField("Build.DEVICE") { Build.DEVICE = it }
applyStringField("Build.DISPLAY") { Build.DISPLAY = it }
applyStringField("Build.FINGERPRINT") { Build.FINGERPRINT = it }
applyStringField("Build.HARDWARE") { Build.HARDWARE = it }
applyStringField("Build.HOST") { Build.HOST = it }
applyStringField("Build.ID") { Build.ID = it }
applyStringField("Build.MANUFACTURER") { Build.MANUFACTURER = it }
applyStringField("Build.MODEL") { Build.MODEL = it }
applyStringField("Build.PRODUCT") { Build.PRODUCT = it }
applyStringField("Build.RADIO") { Build.RADIO = it }
applyStringField("Build.SERIAL") { Build.SERIAL = it }
applyStringField("Build.TAGS") { Build.TAGS = it }
applyLongField("Build.TIME") { Build.TIME = it }
applyStringField("Build.TYPE") { Build.TYPE = it }
applyStringField("Build.USER") { Build.USER = it }
applyStringField("Build.VERSION.CODENAME") { Build.VERSION.CODENAME = it }
applyStringField("Build.VERSION.INCREMENTAL") { Build.VERSION.INCREMENTAL = it }
applyStringField("Build.VERSION.RELEASE") { Build.VERSION.RELEASE = it }
applyStringField("Build.VERSION.SDK") { Build.VERSION.SDK = it }
applyIntField("Build.VERSION.SDK_INT") { Build.VERSION.SDK_INT = it }
applyIntField("Build.VERSION.DEVICE_INITIAL_SDK_INT") { Build.VERSION.DEVICE_INITIAL_SDK_INT = it }
if (android.os.Build.VERSION.SDK_INT >= 21) {
Build.SUPPORTED_ABIS = profileData["Build.SUPPORTED_ABIS"]?.split(",")?.toTypedArray() ?: emptyArray()
} else {
Build.SUPPORTED_ABIS = emptyArray()
}
if (android.os.Build.VERSION.SDK_INT >= 23) {
Build.VERSION.SECURITY_PATCH = profileData["Build.VERSION.SECURITY_PATCH"]
} else {
Build.VERSION.SECURITY_PATCH = null
}
}
private fun applyProfile(context: Context, profile: String, serial: String = getSerial(context, profile)) {
val profileData = getProfileData(context, profile, getRealData())
if (Log.isLoggable(TAG, Log.VERBOSE)) {
for ((key, value) in profileData) {
Log.v(TAG, "<data key=\"$key\" value=\"$value\" />")
}
}
applyProfileData(profileData)
Build.SERIAL = serial
Log.d(TAG, "Using Serial ${Build.SERIAL}")
activeProfile = profile
}
private fun applyRemoteProfile(context: Context, packageName: String) {
val profileData = getRemoteProfileData(context, packageName)
if (Log.isLoggable(TAG, Log.VERBOSE)) {
for ((key, value) in profileData) {
Log.v(TAG, "<data key=\"$key\" value=\"$value\" />")
}
}
if (profileData.isNotEmpty()) {
applyProfileData(profileData)
activeProfile = PROFILE_REMOTE
}
}
fun getProfileName(context: Context, profile: String): String? = getProfileName { getProfileXml(context, profile) }
private fun getProfileName(parserCreator: () -> XmlResourceParser?): String? {
val parser = parserCreator()
if (parser != null) {
try {
var next = parser.next()
while (next != XmlPullParser.END_DOCUMENT) {
when (next) {
XmlPullParser.START_TAG -> when (parser.name) {
"profile" -> {
return parser.getAttributeValue(null, "name")
}
}
}
next = parser.next()
}
} finally {
parser.close()
}
}
return null
}
fun setProfile(context: Context, profile: String?) {
val changed = getProfile(context) != profile
val newProfile = profile ?: PROFILE_AUTO
val newSerial = if (changed) getSerial(context, newProfile, true) else getSerial(context)
SettingsContract.setSettings(context, Profile.getContentUri(context)) {
put(Profile.PROFILE, newProfile)
if (changed) put(Profile.SERIAL, newSerial)
}
if (changed && activeProfile != null) applyProfile(context, newProfile, newSerial)
}
fun importUserProfile(context: Context, file: File): Boolean {
val profileName = getProfileName { FileXmlResourceParser(file) } ?: return false
try {
Log.d(TAG, "Importing user profile '$profileName'")
file.copyTo(getUserProfileFile(context), overwrite = true)
if (activeProfile == PROFILE_USER) applyProfile(context, PROFILE_USER)
return true
} catch (e: Exception) {
Log.w(TAG, e)
return false
}
}
@JvmStatic
fun resetActiveProfile() {
activeProfile = null
}
@JvmStatic
fun ensureInitialized(context: Context) {
val metaData = runCatching { context.packageManager.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA).metaData }.getOrNull() ?: Bundle.EMPTY
synchronized(this) {
try {
if (metaData.containsKey(META_DATA_KEY_SOURCE_PACKAGE)) {
if (activeProfile != PROFILE_REMOTE) {
val packageName = metaData.getString(META_DATA_KEY_SOURCE_PACKAGE)!!
applyRemoteProfile(context, packageName)
}
} else {
val profile = getProfile(context)
if (activeProfile == profile) return
applyProfile(context, profile)
}
} catch (e: Exception) {
Log.w(TAG, e)
}
Unit
}
}
}

View file

@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.settings
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager.GET_META_DATA
import android.os.Bundle
class MetaDataPreferences(private val context: Context, private val prefix: String = "") : SharedPreferences {
private val metaData by lazy {
runCatching { context.packageManager.getApplicationInfo(context.packageName, GET_META_DATA) }.getOrNull()?.metaData ?: Bundle.EMPTY
}
override fun getAll(): Map<String, *> = metaData.keySet().filter { it.startsWith(prefix) }.associate { it.substring(prefix.length) to metaData.get(it) }
override fun getString(key: String, defValue: String?): String? = metaData.getString(prefix + key, defValue)
override fun getStringSet(key: String, defValues: Set<String>?): Set<String>? = metaData.getStringArray(prefix + key)?.toSet() ?: defValues
override fun getInt(key: String?, defValue: Int): Int = metaData.getInt(prefix + key, defValue)
override fun getLong(key: String?, defValue: Long): Long = metaData.getLong(prefix + key, defValue)
override fun getFloat(key: String?, defValue: Float): Float = metaData.getFloat(prefix + key, defValue)
override fun getBoolean(key: String?, defValue: Boolean): Boolean = metaData.getBoolean(prefix + key, defValue)
override fun contains(key: String?): Boolean = metaData.containsKey(prefix + key)
override fun edit(): SharedPreferences.Editor {
throw UnsupportedOperationException()
}
override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener?) {
throw UnsupportedOperationException()
}
override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener?) {
throw UnsupportedOperationException()
}
}

View file

@ -0,0 +1,349 @@
/*
* SPDX-FileCopyrightText: 2021, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.settings
import android.content.ContentValues
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.CrossProfileApps
import android.content.pm.PackageManager
import android.database.Cursor
import android.net.Uri
import android.os.Binder
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.os.UserManager
import android.util.Log
import androidx.core.net.toUri
import org.microg.gms.crossprofile.CrossProfileRequestActivity
import org.microg.gms.ui.TAG
object SettingsContract {
const val META_DATA_KEY_SOURCE_PACKAGE = "org.microg.gms.settings:source-package"
/**
* Stores keys that are useful only for connecting to the SettingsProvider from
* main profile in a managed / work profile
*/
const val CROSS_PROFILE_SHARED_PREFERENCES_NAME = "crossProfile"
const val CROSS_PROFILE_PERMISSION = "uri"
fun getAuthority(context: Context): String {
val metaData = runCatching { context.packageManager.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA).metaData }.getOrNull() ?: Bundle.EMPTY
val sourcePackage = metaData.getString(META_DATA_KEY_SOURCE_PACKAGE, context.packageName)
return "${sourcePackage}.microg.settings"
}
/**
* URI for preferences local to this profile
*/
fun getAuthorityUri(context: Context) = "content://${getAuthority(context)}".toUri()
/* Cross-profile interactivity, granting access to same preferences across all profiles of a user:
* URI points to our `SettingsProvider` on normal profile and is supposed to point to
* _primary_ profile's `SettingsProvider` work / managed profile. If this is not yet established,
* we need to start the `CrossProfileRequestActivity`, which asks `CrossProfileSendActivity` to
* send it a URI that entitles it to access the primary profile's settings. (This would normally
* happen while creating the profile from `UserInitReceiver`.)
*/
fun getCrossProfileSharedAuthorityUri(context: Context): Uri {
if (SDK_INT < 30) {
Log.v(TAG, "cross-profile interactivity not possible on this Android version")
return "content://${getAuthority(context)}".toUri()
}
val userManager = context.getSystemService(UserManager::class.java)
val workProfile = userManager.isManagedProfile
if (!workProfile) {
return "content://${getAuthority(context)}".toUri()
}
/* Check special shared preferences file if it contains a URI that permits us to access
* main profile's settings content provider
*/
val preferences = context.getSharedPreferences(CROSS_PROFILE_SHARED_PREFERENCES_NAME, MODE_PRIVATE)
if (preferences.contains(CROSS_PROFILE_PERMISSION)) {
Log.v(TAG, "using work profile stored URI")
return preferences.getString(CROSS_PROFILE_PERMISSION, null)!!.toUri()
}
val crossProfileApps = context.getSystemService(CrossProfileApps::class.java)
val targetProfiles = crossProfileApps.targetUserProfiles
if (!crossProfileApps.canInteractAcrossProfiles() || targetProfiles.isEmpty()) {
Log.w(TAG, "prerequisites for cross-profile interactivity not met: " +
"can interact = ${crossProfileApps.canInteractAcrossProfiles()}, " +
"#targetProfiles = ${targetProfiles.size}")
return "content://${getAuthority(context)}".toUri()
} else {
Log.d(TAG, "Initiating activity to request storage URI from main profile")
context.startActivity(Intent(context, CrossProfileRequestActivity::class.java).apply {
addFlags(FLAG_ACTIVITY_NEW_TASK)
})
// while proper response is not yet available, work on local data :(
return "content://${getAuthority(context)}".toUri()
}
}
object CheckIn {
const val ID = "check-in"
fun getContentUri(context: Context) = Uri.withAppendedPath(getAuthorityUri(context), ID)
fun getContentType(context: Context) = "vnd.android.cursor.item/vnd.${getAuthority(context)}.$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 {
const val ID = "gcm"
fun getContentUri(context: Context) = Uri.withAppendedPath(getAuthorityUri(context), ID)
fun getContentType(context: Context) = "vnd.android.cursor.item/vnd.${getAuthority(context)}.$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 {
const val ID = "auth"
fun getContentUri(context: Context) = Uri.withAppendedPath(getAuthorityUri(context), ID)
fun getContentType(context: Context) = "vnd.android.cursor.item/vnd.${getAuthority(context)}.$ID"
const val TRUST_GOOGLE = "auth_manager_trust_google"
const val VISIBLE = "auth_manager_visible"
const val INCLUDE_ANDROID_ID = "auth_include_android_id"
const val STRIP_DEVICE_NAME = "auth_strip_device_name"
const val TWO_STEP_VERIFICATION = "auth_two_step_verification"
val PROJECTION = arrayOf(
TRUST_GOOGLE,
VISIBLE,
INCLUDE_ANDROID_ID,
STRIP_DEVICE_NAME,
TWO_STEP_VERIFICATION,
)
}
object Exposure {
const val ID = "exposureNotification"
fun getContentUri(context: Context) = Uri.withAppendedPath(getAuthorityUri(context), ID)
fun getContentType(context: Context) = "vnd.android.cursor.item/vnd.${getAuthority(context)}.$ID"
const val SCANNER_ENABLED = "exposure_scanner_enabled"
const val LAST_CLEANUP = "exposure_last_cleanup"
val PROJECTION = arrayOf(
SCANNER_ENABLED,
LAST_CLEANUP,
)
}
object SafetyNet {
const val ID = "safety-net"
fun getContentUri(context: Context) = Uri.withAppendedPath(getAuthorityUri(context), ID)
fun getContentType(context: Context) = "vnd.android.cursor.item/vnd.${getAuthority(context)}.$ID"
const val ENABLED = "safetynet_enabled"
val PROJECTION = arrayOf(
ENABLED
)
}
object DroidGuard {
const val ID = "droidguard"
fun getContentUri(context: Context) = Uri.withAppendedPath(getAuthorityUri(context), ID)
fun getContentType(context: Context) = "vnd.android.cursor.item/vnd.${getAuthority(context)}.$ID"
const val ENABLED = "droidguard_enabled"
const val MODE = "droidguard_mode"
const val NETWORK_SERVER_URL = "droidguard_network_server_url"
const val FORCE_LOCAL_DISABLED = "droidguard_force_local_disabled"
const val HARDWARE_ATTESTATION_BLOCKED = "droidguard_block_hw_attestation"
val PROJECTION = arrayOf(
ENABLED,
MODE,
NETWORK_SERVER_URL,
FORCE_LOCAL_DISABLED,
HARDWARE_ATTESTATION_BLOCKED,
)
}
object Profile {
const val ID = "profile"
fun getContentUri(context: Context) = Uri.withAppendedPath(getAuthorityUri(context), ID)
fun getContentType(context: Context) = "vnd.android.cursor.item/vnd.${getAuthority(context)}.$ID"
const val PROFILE = "device_profile"
const val SERIAL = "device_profile_serial"
val PROJECTION = arrayOf(
PROFILE,
SERIAL
)
}
object Location {
const val ID = "location"
fun getContentUri(context: Context) = Uri.withAppendedPath(getAuthorityUri(context), ID)
fun getContentType(context: Context) = "vnd.android.cursor.item/vnd.${getAuthority(context)}.$ID"
const val WIFI_ICHNAEA = "location_wifi_mls"
const val WIFI_MOVING = "location_wifi_moving"
const val WIFI_LEARNING = "location_wifi_learning"
const val WIFI_CACHING = "location_wifi_caching"
const val CELL_ICHNAEA = "location_cell_mls"
const val CELL_LEARNING = "location_cell_learning"
const val CELL_CACHING = "location_cell_caching"
const val GEOCODER_NOMINATIM = "location_geocoder_nominatim"
const val ICHNAEA_ENDPOINT = "location_ichnaea_endpoint"
const val ONLINE_SOURCE = "location_online_source"
const val ICHNAEA_CONTRIBUTE = "location_ichnaea_contribute"
val PROJECTION = arrayOf(
WIFI_ICHNAEA,
WIFI_MOVING,
WIFI_LEARNING,
WIFI_CACHING,
CELL_ICHNAEA,
CELL_LEARNING,
CELL_CACHING,
GEOCODER_NOMINATIM,
ICHNAEA_ENDPOINT,
ONLINE_SOURCE,
ICHNAEA_CONTRIBUTE,
)
}
object Vending {
const val ID = "vending"
fun getContentUri(context: Context) = Uri.withAppendedPath(getAuthorityUri(context), ID)
fun getContentType(context: Context) = "vnd.android.cursor.item/vnd.${getAuthority(context)}.$ID"
const val LICENSING = "vending_licensing"
const val LICENSING_PURCHASE_FREE_APPS = "vending_licensing_purchase_free_apps"
const val SPLIT_INSTALL = "vending_split_install"
const val BILLING = "vending_billing"
const val ASSET_DELIVERY = "vending_asset_delivery"
const val ASSET_DEVICE_SYNC = "vending_device_sync"
const val APPS_INSTALL = "vending_apps_install"
const val APPS_INSTALLER_LIST = "vending_apps_installer_list"
val PROJECTION = arrayOf(
LICENSING,
LICENSING_PURCHASE_FREE_APPS,
SPLIT_INSTALL,
BILLING,
ASSET_DELIVERY,
ASSET_DEVICE_SYNC,
APPS_INSTALL,
APPS_INSTALLER_LIST,
)
}
object WorkProfile {
const val ID = "workprofile"
fun getContentUri(context: Context) = Uri.withAppendedPath(getCrossProfileSharedAuthorityUri(context), ID)
fun getContentType(context: Context) = "vnd.android.cursor.item/vnd.${getAuthority(context)}.$ID"
const val CREATE_WORK_ACCOUNT = "workprofile_allow_create_work_account"
val PROJECTION = arrayOf(
CREATE_WORK_ACCOUNT
)
}
object GameProfile {
const val ID = "gameprofile"
fun getContentUri(context: Context) = Uri.withAppendedPath(getCrossProfileSharedAuthorityUri(context), ID)
fun getContentType(context: Context) = "vnd.android.cursor.item/vnd.${getAuthority(context)}.$ID"
const val ALLOW_CREATE_PLAYER = "game_allow_create_player"
const val ALLOW_UPLOAD_GAME_PLAYED = "allow_upload_game_played"
val PROJECTION = arrayOf(
ALLOW_CREATE_PLAYER,
ALLOW_UPLOAD_GAME_PLAYED
)
}
private fun <T> withoutCallingIdentity(f: () -> T): T {
val identity = Binder.clearCallingIdentity()
try {
return f.invoke()
} finally {
Binder.restoreCallingIdentity(identity)
}
}
@JvmStatic
fun <T> getSettings(context: Context, uri: Uri, projection: Array<out String>?, f: (Cursor) -> T): T = withoutCallingIdentity {
val c = context.contentResolver.query(uri, projection, null, null, null)
try {
require(c != null) { "Cursor for query $uri ${projection?.toList()} was null" }
if (!c.moveToFirst()) error("Cursor for query $uri ${projection?.toList()} was empty")
f.invoke(c)
} finally {
c?.close()
}
}
@JvmStatic
fun setSettings(context: Context, uri: Uri, v: ContentValues.() -> Unit) = withoutCallingIdentity {
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

@ -0,0 +1,494 @@
/*
* SPDX-FileCopyrightText: 2021, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
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.os.Build.VERSION.SDK_INT
import android.preference.PreferenceManager
import org.microg.gms.common.PackageUtils.warnIfNotMainProcess
import org.microg.gms.settings.SettingsContract.Auth
import org.microg.gms.settings.SettingsContract.CheckIn
import org.microg.gms.settings.SettingsContract.DroidGuard
import org.microg.gms.settings.SettingsContract.Exposure
import org.microg.gms.settings.SettingsContract.GameProfile
import org.microg.gms.settings.SettingsContract.Gcm
import org.microg.gms.settings.SettingsContract.Location
import org.microg.gms.settings.SettingsContract.Profile
import org.microg.gms.settings.SettingsContract.SafetyNet
import org.microg.gms.settings.SettingsContract.Vending
import org.microg.gms.settings.SettingsContract.WorkProfile
import org.microg.gms.settings.SettingsContract.getAuthority
import java.io.File
private const val SETTINGS_PREFIX = "org.microg.gms.settings."
/**
* 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 unifiedNlpPreferences by lazy {
context!!.getSharedPreferences("unified_nlp", 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
}
}
private val metaDataPreferences: SharedPreferences by lazy {
MetaDataPreferences(context!!, SETTINGS_PREFIX)
}
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.pathSegments.last()) {
CheckIn.ID -> queryCheckIn(projection ?: CheckIn.PROJECTION)
Gcm.ID -> queryGcm(projection ?: Gcm.PROJECTION)
Auth.ID -> queryAuth(projection ?: Auth.PROJECTION)
Exposure.ID -> queryExposure(projection ?: Exposure.PROJECTION)
SafetyNet.ID -> querySafetyNet(projection ?: SafetyNet.PROJECTION)
DroidGuard.ID -> queryDroidGuard(projection ?: DroidGuard.PROJECTION)
Profile.ID -> queryProfile(projection ?: Profile.PROJECTION)
Location.ID -> queryLocation(projection ?: Location.PROJECTION)
Vending.ID -> queryVending(projection ?: Vending.PROJECTION)
WorkProfile.ID -> queryWorkProfile(projection ?: WorkProfile.PROJECTION)
GameProfile.ID -> queryGameProfile(projection ?: GameProfile.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.pathSegments.last()) {
CheckIn.ID -> updateCheckIn(values)
Gcm.ID -> updateGcm(values)
Auth.ID -> updateAuth(values)
Exposure.ID -> updateExposure(values)
SafetyNet.ID -> updateSafetyNet(values)
DroidGuard.ID -> updateDroidGuard(values)
Profile.ID -> updateProfile(values)
Location.ID -> updateLocation(values)
Vending.ID -> updateVending(values)
WorkProfile.ID -> updateWorkProfile(values)
GameProfile.ID -> updateGameProfile(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, false)
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) ->
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) ->
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)
Auth.INCLUDE_ANDROID_ID -> getSettingsBoolean(key, true)
Auth.STRIP_DEVICE_NAME -> getSettingsBoolean(key, false)
Auth.TWO_STEP_VERIFICATION -> 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) ->
when (key) {
Auth.TRUST_GOOGLE -> editor.putBoolean(key, value as Boolean)
Auth.VISIBLE -> editor.putBoolean(key, value as Boolean)
Auth.INCLUDE_ANDROID_ID -> editor.putBoolean(key, value as Boolean)
Auth.STRIP_DEVICE_NAME -> editor.putBoolean(key, value as Boolean)
Auth.TWO_STEP_VERIFICATION -> 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) ->
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 querySafetyNet(p: Array<out String>): Cursor = MatrixCursor(p).addRow(p) { key ->
when (key) {
SafetyNet.ENABLED -> getSettingsBoolean(key, false)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
private fun updateSafetyNet(values: ContentValues) {
if (values.size() == 0) return
val editor = preferences.edit()
values.valueSet().forEach { (key, value) ->
when (key) {
SafetyNet.ENABLED -> editor.putBoolean(key, value as Boolean)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
editor.apply()
}
private fun queryDroidGuard(p: Array<out String>): Cursor = MatrixCursor(p).addRow(p) { key ->
when (key) {
DroidGuard.ENABLED -> getSettingsBoolean(key, false)
DroidGuard.MODE -> getSettingsString(key)
DroidGuard.NETWORK_SERVER_URL -> getSettingsString(key)
DroidGuard.FORCE_LOCAL_DISABLED -> systemDefaultPreferences?.getBoolean(key, false) ?: false
DroidGuard.HARDWARE_ATTESTATION_BLOCKED -> getSettingsBoolean(key, true)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
private fun updateDroidGuard(values: ContentValues) {
if (values.size() == 0) return
val editor = preferences.edit()
values.valueSet().forEach { (key, value) ->
when (key) {
DroidGuard.ENABLED -> editor.putBoolean(key, value as Boolean)
DroidGuard.MODE -> editor.putString(key, value as String)
DroidGuard.NETWORK_SERVER_URL -> editor.putString(key, value as String)
DroidGuard.HARDWARE_ATTESTATION_BLOCKED -> editor.putBoolean(key, value as Boolean)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
editor.apply()
}
private fun queryProfile(p: Array<out String>): Cursor = MatrixCursor(p).addRow(p) { key ->
when (key) {
Profile.PROFILE -> getSettingsString(key, "auto")
Profile.SERIAL -> getSettingsString(key)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
private fun updateProfile(values: ContentValues) {
if (values.size() == 0) return
val editor = preferences.edit()
values.valueSet().forEach { (key, value) ->
when (key) {
Profile.PROFILE -> editor.putString(key, value as String?)
Profile.SERIAL -> editor.putString(key, value as String?)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
editor.apply()
}
private fun queryLocation(p: Array<out String>): Cursor = MatrixCursor(p).addRow(p) { key ->
when (key) {
Location.WIFI_ICHNAEA -> getSettingsBoolean(key, hasUnifiedNlpLocationBackend("org.microg.nlp.backend.ichnaea"))
Location.WIFI_MOVING -> getSettingsBoolean(key, hasUnifiedNlpLocationBackend("de.sorunome.unifiednlp.trains"))
Location.WIFI_LEARNING -> getSettingsBoolean(key, false)
Location.WIFI_CACHING -> getSettingsBoolean(key, getSettingsBoolean(Location.WIFI_LEARNING, false) == 1)
Location.CELL_ICHNAEA -> getSettingsBoolean(key, hasUnifiedNlpLocationBackend("org.microg.nlp.backend.ichnaea"))
Location.CELL_LEARNING -> getSettingsBoolean(key, true)
Location.CELL_CACHING -> getSettingsBoolean(key, getSettingsBoolean(Location.CELL_LEARNING, true) == 1)
Location.GEOCODER_NOMINATIM -> getSettingsBoolean(key, hasUnifiedNlpGeocoderBackend("org.microg.nlp.backend.nominatim") )
Location.ICHNAEA_ENDPOINT -> getSettingsString(key, null)
Location.ONLINE_SOURCE -> getSettingsString(key, null)
Location.ICHNAEA_CONTRIBUTE -> getSettingsBoolean(key, false)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
private fun hasUnifiedNlpPrefixInStringSet(key: String, vararg prefixes: String) = getUnifiedNlpSettingsStringSetCompat(key, emptySet()).any { entry -> prefixes.any { prefix -> entry.startsWith(prefix)}}
private fun hasUnifiedNlpLocationBackend(vararg packageNames: String) = hasUnifiedNlpPrefixInStringSet("location_backends", *packageNames.map { "$it/" }.toTypedArray())
private fun hasUnifiedNlpGeocoderBackend(vararg packageNames: String) = hasUnifiedNlpPrefixInStringSet("geocoder_backends", *packageNames.map { "$it/" }.toTypedArray())
private fun updateLocation(values: ContentValues) {
if (values.size() == 0) return
val editor = preferences.edit()
values.valueSet().forEach { (key, value) ->
when (key) {
Location.WIFI_ICHNAEA -> editor.putBoolean(key, value as Boolean)
Location.WIFI_MOVING -> editor.putBoolean(key, value as Boolean)
Location.WIFI_LEARNING -> editor.putBoolean(key, value as Boolean)
Location.CELL_ICHNAEA -> editor.putBoolean(key, value as Boolean)
Location.CELL_LEARNING -> editor.putBoolean(key, value as Boolean)
Location.GEOCODER_NOMINATIM -> editor.putBoolean(key, value as Boolean)
Location.ICHNAEA_ENDPOINT -> (value as String).let { if (it.isBlank()) editor.remove(key) else editor.putString(key, it) }
Location.ONLINE_SOURCE -> (value as? String?).let { if (it.isNullOrBlank()) editor.remove(key) else editor.putString(key, it) }
Location.ICHNAEA_CONTRIBUTE -> editor.putBoolean(key, value as Boolean)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
editor.apply()
}
private fun queryVending(p: Array<out String>): Cursor = MatrixCursor(p).addRow(p) { key ->
when (key) {
Vending.LICENSING -> getSettingsBoolean(key, false)
Vending.LICENSING_PURCHASE_FREE_APPS -> getSettingsBoolean(key, false)
Vending.BILLING -> getSettingsBoolean(key, false)
Vending.ASSET_DELIVERY -> getSettingsBoolean(key, false)
Vending.ASSET_DEVICE_SYNC -> getSettingsBoolean(key, false)
Vending.SPLIT_INSTALL -> getSettingsBoolean(key, false)
Vending.APPS_INSTALL -> getSettingsBoolean(key, false)
Vending.APPS_INSTALLER_LIST -> getSettingsString(key, "")
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
private fun updateVending(values: ContentValues) {
if (values.size() == 0) return
val editor = preferences.edit()
values.valueSet().forEach { (key, value) ->
when (key) {
Vending.LICENSING -> editor.putBoolean(key, value as Boolean)
Vending.LICENSING_PURCHASE_FREE_APPS -> editor.putBoolean(key, value as Boolean)
Vending.BILLING -> editor.putBoolean(key, value as Boolean)
Vending.SPLIT_INSTALL -> editor.putBoolean(key, value as Boolean)
Vending.ASSET_DELIVERY -> editor.putBoolean(key, value as Boolean)
Vending.ASSET_DEVICE_SYNC -> editor.putBoolean(key, value as Boolean)
Vending.APPS_INSTALL -> editor.putBoolean(key, value as Boolean)
Vending.APPS_INSTALLER_LIST -> editor.putString(key, value as String)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
editor.apply()
}
private fun queryWorkProfile(p: Array<out String>): Cursor = MatrixCursor(p).addRow(p) { key ->
when (key) {
WorkProfile.CREATE_WORK_ACCOUNT -> getSettingsBoolean(key, false)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
private fun updateWorkProfile(values: ContentValues) {
if (values.size() == 0) return
val editor = preferences.edit()
values.valueSet().forEach { (key, value) ->
when (key) {
WorkProfile.CREATE_WORK_ACCOUNT -> editor.putBoolean(key, value as Boolean)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
editor.apply()
}
private fun queryGameProfile(p: Array<out String>): Cursor = MatrixCursor(p).addRow(p) { key ->
when (key) {
GameProfile.ALLOW_CREATE_PLAYER -> getSettingsBoolean(key, false)
GameProfile.ALLOW_UPLOAD_GAME_PLAYED -> getSettingsBoolean(key, false)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
private fun updateGameProfile(values: ContentValues) {
if (values.size() == 0) return
val editor = preferences.edit()
values.valueSet().forEach { (key, value) ->
when (key) {
GameProfile.ALLOW_CREATE_PLAYER -> editor.putBoolean(key, value as Boolean)
GameProfile.ALLOW_UPLOAD_GAME_PLAYED -> editor.putBoolean(key, value as Boolean)
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))
return this
}
override fun getType(uri: Uri): String {
return "vnd.android.cursor.item/vnd.${getAuthority(context!!)}.${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 {
return listOf(preferences, systemDefaultPreferences, metaDataPreferences).getBooleanAsInt(key, def)
}
private fun getSettingsString(key: String, def: String? = null): String? = listOf(preferences, systemDefaultPreferences, metaDataPreferences).getString(key, def)
private fun getSettingsInt(key: String, def: Int): Int = listOf(preferences, systemDefaultPreferences, metaDataPreferences).getInt(key, def)
private fun getSettingsLong(key: String, def: Long): Long = listOf(preferences, systemDefaultPreferences, metaDataPreferences).getLong(key, def)
private fun getUnifiedNlpSettingsStringSetCompat(key: String, def: Set<String>): Set<String> = listOf(unifiedNlpPreferences, preferences, systemDefaultPreferences).getStringSetCompat(key, def)
private fun SharedPreferences.getStringSetCompat(key: String, def: Set<String>): Set<String> {
if (SDK_INT >= 11) {
try {
val res = getStringSet(key, null)
if (res != null) return res.filter { it.isNotEmpty() }.toSet()
} catch (ignored: Exception) {
// Ignore
}
}
try {
val str = getString(key, null)
if (str != null) return str.split("\\|".toRegex()).filter { it.isNotEmpty() }.toSet()
} catch (ignored: Exception) {
// Ignore
}
return def
}
private fun List<SharedPreferences?>.getStringSetCompat(key: String, def: Set<String>): Set<String> = foldRight(def) { preferences, defValue -> preferences?.getStringSetCompat(key, defValue) ?: defValue }
private fun List<SharedPreferences?>.getString(key: String, def: String?): String? = foldRight(def) { preferences, defValue -> preferences?.getString(key, defValue) ?: defValue }
private fun List<SharedPreferences?>.getInt(key: String, def: Int): Int = foldRight(def) { preferences, defValue -> preferences?.getInt(key, defValue) ?: defValue }
private fun List<SharedPreferences?>.getLong(key: String, def: Long): Long = foldRight(def) { preferences, defValue -> preferences?.getLong(key, defValue) ?: defValue }
private fun List<SharedPreferences?>.getBoolean(key: String, def: Boolean): Boolean = foldRight(def) { preferences, defValue -> preferences?.getBoolean(key, defValue) ?: defValue }
private fun List<SharedPreferences?>.getBooleanAsInt(key: String, def: Boolean): Int = if (getBoolean(key, def)) 1 else 0
}

View file

@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.net.Uri
import android.provider.Settings
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.widget.ImageView
import androidx.appcompat.content.res.AppCompatResources
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import org.microg.gms.base.core.R
class AppHeadingPreference : AppPreference, Preference.OnPreferenceClickListener {
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context) : super(context)
init {
layoutResource = R.layout.preference_app_heading
onPreferenceClickListener = this
}
override fun onPreferenceClick(preference: Preference): Boolean {
if (packageName != null) {
val intent = Intent()
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
val uri: Uri = Uri.fromParts("package", packageName, null)
intent.data = uri
try {
context.startActivity(intent)
} catch (e: Exception) {
Log.w(TAG, "Failed to launch app", e)
}
return true
} else {
return false
}
}
}

View file

@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
import android.content.Context
import android.content.pm.ApplicationInfo
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.widget.ImageView
import androidx.appcompat.content.res.AppCompatResources
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
class AppIconPreference : AppPreference {
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context) : super(context)
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
val icon = holder.findViewById(android.R.id.icon)
if (icon is ImageView) {
icon.adjustViewBounds = true
icon.scaleType = ImageView.ScaleType.CENTER_INSIDE
icon.maxHeight = (32.0 * context.resources.displayMetrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT).toInt()
}
}
}

View file

@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
import android.content.Context
import android.content.pm.ApplicationInfo
import android.util.AttributeSet
import androidx.appcompat.content.res.AppCompatResources
import androidx.preference.Preference
abstract class AppPreference : Preference {
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context) : super(context)
init {
isPersistent = false
}
private var packageNameField: String? = null
var applicationInfo: ApplicationInfo?
get() = context.packageManager.getApplicationInfoIfExists(packageNameField)
set(value) {
if (value == null && packageNameField != null) {
title = null
icon = null
} else if (value != null) {
val pm = context.packageManager
title = value.loadLabel(pm) ?: value.packageName
icon = value.loadIcon(pm) ?: AppCompatResources.getDrawable(context, android.R.mipmap.sym_def_app_icon)
}
packageNameField = value?.packageName
}
var packageName: String?
get() = packageNameField
set(value) {
if (value == null && packageNameField != null) {
title = null
icon = null
} else if (value != null) {
val pm = context.packageManager
val applicationInfo = pm.getApplicationInfoIfExists(value)
title = applicationInfo?.loadLabel(pm)?.toString() ?: value
icon = applicationInfo?.loadIcon(pm) ?: AppCompatResources.getDrawable(context, android.R.mipmap.sym_def_app_icon)
}
packageNameField = value
}
}

View file

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
const val TAG = "GmsUi"

View file

@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
import android.content.Context
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.LinearLayout
import android.widget.TextView
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import org.microg.gms.base.core.R
class FooterPreference : Preference {
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context) : super(context)
init {
layoutResource = R.layout.preference_footer
if (icon == null) setIcon(R.drawable.ic_info_outline)
}
}

View file

@ -0,0 +1,62 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import androidx.appcompat.widget.SwitchCompat
import androidx.core.content.res.TypedArrayUtils
import androidx.preference.PreferenceViewHolder
import androidx.preference.TwoStatePreference
import org.microg.gms.base.core.R
// TODO
class SwitchBarPreference : TwoStatePreference {
private val frameId: Int
private val backgroundOn: Drawable?
private val backgroundOff: Drawable?
private val backgroundDisabled: Drawable?
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
val a = context.obtainStyledAttributes(attrs, R.styleable.SwitchBarPreference, defStyleAttr, defStyleRes)
frameId = a.getResourceId(R.styleable.SwitchBarPreference_switchBarFrameId, 0)
backgroundOn = a.getDrawable(R.styleable.SwitchBarPreference_switchBarFrameBackgroundOn)
backgroundOff = a.getDrawable(R.styleable.SwitchBarPreference_switchBarFrameBackgroundOff)
backgroundDisabled = a.getDrawable(R.styleable.SwitchBarPreference_switchBarFrameBackgroundDisabled)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : this(context, attrs, defStyleAttr, R.style.Preference_SwitchBar)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, R.attr.switchBarPreferenceStyle)
constructor(context: Context) : this(context, null)
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
holder.isDividerAllowedBelow = false
holder.isDividerAllowedAbove = false
val switch = holder.findViewById(R.id.switch_widget) as SwitchCompat
switch.setOnCheckedChangeListener(null)
switch.isChecked = isChecked
switch.setOnCheckedChangeListener { view, isChecked ->
if (!callChangeListener(isChecked)) {
view.isChecked = !isChecked
return@setOnCheckedChangeListener
}
this.isChecked = isChecked
}
val frame = if (frameId == 0) null else holder.findViewById(frameId)
val backgroundView = frame ?: holder.itemView
val (backgroundDrawable, backgroundColorAttribute) = when {
!isEnabled -> Pair(backgroundDisabled, androidx.appcompat.R.attr.colorControlHighlight)
isChecked -> Pair(backgroundOn, androidx.appcompat.R.attr.colorControlActivated)
else -> Pair(backgroundOff, androidx.appcompat.R.attr.colorButtonNormal)
}
if (backgroundDrawable != null) {
backgroundView.setBackgroundDrawable(backgroundDrawable)
} else {
backgroundView.setBackgroundColorAttribute(backgroundColorAttribute)
}
}
}

View file

@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
import android.content.Context
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.LinearLayout
import android.widget.TextView
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
class TextPreference : Preference {
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context) : super(context)
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
val iconFrame = holder?.findViewById(androidx.preference.R.id.icon_frame)
iconFrame?.layoutParams?.height = MATCH_PARENT
(iconFrame as? LinearLayout)?.gravity = Gravity.TOP or Gravity.START
val pad = (context.resources.displayMetrics.densityDpi/160f * 20).toInt()
iconFrame?.setPadding(0, pad, 0, pad)
val textView = holder?.findViewById(android.R.id.summary) as? TextView
textView?.maxLines = Int.MAX_VALUE
}
}

View file

@ -0,0 +1,78 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.util.TypedValue
import android.view.View
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.annotation.IdRes
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.databinding.BindingAdapter
import androidx.navigation.NavController
import androidx.navigation.navOptions
import androidx.navigation.ui.R
import com.google.android.material.dialog.MaterialAlertDialogBuilder
fun PackageManager.getApplicationInfoIfExists(packageName: String?, flags: Int = 0): ApplicationInfo? = packageName?.let {
try {
getApplicationInfo(it, flags)
} catch (e: Exception) {
Log.w(TAG, "Package $packageName not installed.")
null
}
}
fun NavController.navigate(context: Context, @IdRes resId: Int, args: Bundle? = null) {
navigate(resId, args, if (context.systemAnimationsEnabled) navOptions {
anim {
enter = R.anim.nav_default_enter_anim
exit = R.anim.nav_default_exit_anim
popEnter = R.anim.nav_default_pop_enter_anim
popExit = R.anim.nav_default_pop_exit_anim
}
} else null)
}
val Context.systemAnimationsEnabled: Boolean
get() {
val duration: Float
val transition: Float
if (SDK_INT >= 17) {
duration = Settings.Global.getFloat(contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f)
transition = Settings.Global.getFloat(contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE, 1f)
} else {
duration = Settings.System.getFloat(contentResolver, Settings.System.ANIMATOR_DURATION_SCALE, 1f)
transition = Settings.System.getFloat(contentResolver, Settings.System.TRANSITION_ANIMATION_SCALE, 1f)
}
return duration != 0f && transition != 0f
}
fun Context.buildAlertDialog() = try {
// Try material design first
MaterialAlertDialogBuilder(this)
} catch (e: Exception) {
AlertDialog.Builder(this)
}
@ColorInt
fun Context.resolveColor(@AttrRes resid: Int): Int? {
val typedValue = TypedValue()
if (!theme.resolveAttribute(resid, typedValue, true)) return null
val colorRes = if (typedValue.resourceId != 0) typedValue.resourceId else typedValue.data
return ContextCompat.getColor(this, colorRes)
}
@BindingAdapter("app:backgroundColorAttr")
fun View.setBackgroundColorAttribute(@AttrRes resId: Int) = context.resolveColor(resId)?.let { setBackgroundColor(it) }

View file

@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui.settings
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.util.Log
import androidx.navigation.NavController
private const val TAG = "SettingsProvider"
interface SettingsProvider {
fun getEntriesStatic(context: Context): List<Entry>
suspend fun getEntriesDynamic(context: Context): List<Entry> = getEntriesStatic(context)
fun preProcessSettingsIntent(intent: Intent)
fun extendNavigation(navController: NavController)
companion object {
enum class Group {
HEADER,
GOOGLE,
OTHER,
FOOTER
}
data class Entry(
val key: String,
val group: Group,
val navigationId: Int,
val title: String,
val summary: String? = null,
val icon: Drawable? = null,
)
}
}
fun getAllSettingsProviders(context: Context): List<SettingsProvider> {
val metaData = runCatching { context.packageManager.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA).metaData }.getOrNull() ?: Bundle.EMPTY
return metaData.keySet().asSequence().filter {
it.startsWith("org.microg.gms.ui.settings.entry:")
}.mapNotNull {
runCatching { metaData.getString(it) }.onFailure { Log.w(TAG, it) }.getOrNull()
}.mapNotNull {
runCatching { Class.forName(it) }.onFailure { Log.w(TAG, it) }.getOrNull()
}.filter {
SettingsProvider::class.java.isAssignableFrom(it)
}.mapNotNull {
runCatching { it.getDeclaredField("INSTANCE").get(null) as SettingsProvider }.onFailure { Log.w(TAG, it) }.getOrNull()
}.toList()
}

View file

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2021, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.utils
import android.os.Binder
import android.os.IBinder
import android.os.Parcel
import android.util.Log
private const val TAG = "BinderUtils"
fun IBinder.warnOnTransactionIssues(code: Int, reply: Parcel?, flags: Int, tag: String = TAG, base: () -> Boolean): Boolean {
if (base.invoke()) {
if ((flags and Binder.FLAG_ONEWAY) > 0 && (reply?.dataSize() ?: 0) > 0) {
Log.w(tag, "Method $code in $interfaceDescriptor is oneway, but returned data")
}
return true
}
Log.w(tag, "Unknown method $code in $interfaceDescriptor, skipping")
return (flags and Binder.FLAG_ONEWAY) > 0 // Don't return false on oneway transaction to suppress warning
}

View file

@ -0,0 +1,33 @@
/**
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.utils
import android.graphics.Bitmap
import kotlin.math.sqrt
object BitmapUtils {
fun getBitmapSize(bitmap: Bitmap?): Int {
if (bitmap != null) {
return bitmap.height * bitmap.rowBytes
}
return 0
}
fun scaledBitmap(bitmap: Bitmap, maxSize: Float): Bitmap {
val height: Int = bitmap.getHeight()
val width: Int = bitmap.getWidth()
val sqrt =
sqrt(((maxSize) / ((width.toFloat()) / (height.toFloat()) * ((bitmap.getRowBytes() / width).toFloat()))).toDouble())
.toInt()
return Bitmap.createScaledBitmap(
bitmap,
(((sqrt.toFloat()) / (height.toFloat()) * (width.toFloat())).toInt()),
sqrt,
true
)
}
}

View file

@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.utils
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.content.pm.PackageInfoCompat
import org.microg.gms.common.*
class ExtendedPackageInfo(private val packageManager: PackageManager, val packageName: String) {
constructor(context: Context, packageName: String) : this(context.packageManager, packageName)
private val basicPackageInfo by lazy { kotlin.runCatching { packageManager.getPackageInfo(packageName, 0) }.getOrNull() }
private val basicApplicationInfo by lazy { kotlin.runCatching { packageManager.getApplicationInfo(packageName, 0) }.getOrNull() }
val isInstalled by lazy { basicPackageInfo != null }
val certificates by lazy { packageManager.getCertificates(packageName) }
private val certificatesHashSha1 by lazy { certificates.map { it.digest("SHA1") } }
val firstCertificateSha1 by lazy { certificatesHashSha1.firstOrNull() }
val firstCertificateSha1Hex by lazy { firstCertificateSha1?.toHexString() }
private val certificatesHashSha256 by lazy { certificates.map { it.digest("SHA-256") } }
val firstCertificateSha256 by lazy { certificatesHashSha256.firstOrNull() }
private val certificatesHashSha1Strings by lazy { certificatesHashSha1.map { it.toHexString() } }
private val certificatesHashSha256Strings by lazy { certificatesHashSha256.map { it.toHexString() } }
val applicationLabel by lazy { packageManager.getApplicationLabel(packageName) }
@Deprecated("version code is now a long", replaceWith = ReplaceWith("versionCode"))
val shortVersionCode by lazy { basicPackageInfo?.versionCode ?: -1 }
val versionCode by lazy { basicPackageInfo?.let { PackageInfoCompat.getLongVersionCode(it) } ?: -1 }
val versionName by lazy { basicPackageInfo?.versionName }
val targetSdkVersion by lazy { basicApplicationInfo?.targetSdkVersion ?: -1 }
private val packageAndCertHashes by lazy {
listOf(
certificatesHashSha1Strings.map { PackageAndCertHash(packageName, "SHA1", it) },
certificatesHashSha256Strings.map { PackageAndCertHash(packageName, "SHA-256", it) },
).flatten()
}
val isGooglePackage by lazy { packageAndCertHashes.any { isGooglePackage(it) } }
val isPlatformPackage by lazy {
val platformCertificates = packageManager.getPlatformCertificates()
certificates.any { it in platformCertificates }
}
val isGoogleOrPlatformPackage by lazy { isGooglePackage || isPlatformPackage }
private val googlePackagePermissions by lazy { packageAndCertHashes.flatMap { getGooglePackagePermissions(it) }.toSet() }
fun hasGooglePackagePermission(permission: GooglePackagePermission) = permission in googlePackagePermissions
}

View file

@ -0,0 +1,127 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.utils
import android.content.res.XmlResourceParser
import android.util.Xml
import org.xmlpull.v1.XmlPullParser
import java.io.Closeable
import java.io.File
import java.io.FileReader
import java.io.Reader
class FileXmlResourceParser(private val reader: Reader, private val parser: XmlPullParser = Xml.newPullParser()) :
XmlResourceParser,
XmlPullParser by parser,
Closeable by reader {
constructor(file: File) : this(FileReader(file))
init {
parser.setInput(reader)
}
override fun getAttributeNameResource(index: Int): Int {
return 0
}
override fun getAttributeListValue(
namespace: String?, attribute: String?,
options: Array<String?>?, defaultValue: Int
): Int {
val s = getAttributeValue(namespace, attribute)
return s?.toInt() ?: defaultValue
}
override fun getAttributeBooleanValue(
namespace: String?, attribute: String?,
defaultValue: Boolean
): Boolean {
val s = getAttributeValue(namespace, attribute)
return s?.toBooleanStrictOrNull() ?: defaultValue
}
override fun getAttributeResourceValue(
namespace: String?, attribute: String?,
defaultValue: Int
): Int {
val s = getAttributeValue(namespace, attribute)
return s?.toInt() ?: defaultValue
}
override fun getAttributeIntValue(
namespace: String?, attribute: String?,
defaultValue: Int
): Int {
val s = getAttributeValue(namespace, attribute)
return s?.toInt() ?: defaultValue
}
override fun getAttributeUnsignedIntValue(
namespace: String?, attribute: String?,
defaultValue: Int
): Int {
val s = getAttributeValue(namespace, attribute)
return s?.toInt() ?: defaultValue
}
override fun getAttributeFloatValue(
namespace: String?, attribute: String?,
defaultValue: Float
): Float {
val s = getAttributeValue(namespace, attribute)
return s?.toFloat() ?: defaultValue
}
override fun getAttributeListValue(
index: Int,
options: Array<String?>?, defaultValue: Int
): Int {
val s = getAttributeValue(index)
return s?.toInt() ?: defaultValue
}
override fun getAttributeBooleanValue(index: Int, defaultValue: Boolean): Boolean {
val s = getAttributeValue(index)
return s?.toBooleanStrictOrNull() ?: defaultValue
}
override fun getAttributeResourceValue(index: Int, defaultValue: Int): Int {
val s = getAttributeValue(index)
return s?.toInt() ?: defaultValue
}
override fun getAttributeIntValue(index: Int, defaultValue: Int): Int {
val s = getAttributeValue(index)
return s?.toInt() ?: defaultValue
}
override fun getAttributeUnsignedIntValue(index: Int, defaultValue: Int): Int {
val s = getAttributeValue(index)
return s?.toInt() ?: defaultValue
}
override fun getAttributeFloatValue(index: Int, defaultValue: Float): Float {
val s = getAttributeValue(index)
return s?.toFloat() ?: defaultValue
}
override fun getIdAttribute(): String? {
return getAttributeValue(null, "id")
}
override fun getClassAttribute(): String? {
return getAttributeValue(null, "class")
}
override fun getIdAttributeResourceValue(defaultValue: Int): Int {
return getAttributeResourceValue(null, "id", defaultValue)
}
override fun getStyleAttribute(): Int {
return getAttributeResourceValue(null, "style", 0)
}
}

View file

@ -0,0 +1,149 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.utils
import android.app.AlarmManager
import android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP
import android.app.PendingIntent.FLAG_NO_CREATE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build.VERSION.SDK_INT
import android.os.Parcelable
import android.os.SystemClock
import android.util.Log
import androidx.core.app.PendingIntentCompat
import androidx.core.content.getSystemService
import java.util.UUID
class IntentCacheManager<S : Service, T : Parcelable>(private val context: Context, private val clazz: Class<S>, private val type: Int) {
private val lock = Any()
private lateinit var content: ArrayList<T>
private lateinit var id: String
private var isReady: Boolean = false
private val pendingActions: MutableList<() -> Unit> = arrayListOf()
init {
val pendingIntent = PendingIntentCompat.getService(context, type, getIntent(), 0, true)!!
val alarmManager = context.getSystemService<AlarmManager>()
if (SDK_INT >= 19) {
alarmManager?.setWindow(ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + TEN_YEARS, -1, pendingIntent)
} else {
alarmManager?.set(ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + TEN_YEARS, pendingIntent)
}
pendingIntent.send()
}
private fun getIntent() = Intent(context, clazz).apply {
action = ACTION
putExtra(EXTRA_IS_CACHE, true)
putExtra(EXTRA_CACHE_TYPE, this@IntentCacheManager.type)
}
fun add(entry: T, check: (T) -> Boolean = { false }) = runIfReady {
val iterator = content.iterator()
while (iterator.hasNext()) {
if (check(iterator.next())) {
iterator.remove()
}
}
content.add(entry)
updateIntent()
}
fun remove(entry: T) = runIfReady {
if (content.remove(entry)) updateIntent()
}
fun removeIf(check: (T) -> Boolean) = runIfReady {
var removed = false
val iterator = content.iterator()
while (iterator.hasNext()) {
if (check(iterator.next())) {
iterator.remove()
removed = true
}
}
if (removed) updateIntent()
}
fun clear() = runIfReady {
content.clear()
updateIntent()
}
fun getId(): String? = if (this::id.isInitialized) id else null
fun getEntries(): List<T> = if (this::content.isInitialized) content else emptyList()
fun processIntent(intent: Intent) {
if (isCache(intent) && getType(intent) == type) {
synchronized(lock) {
content = intent.getParcelableArrayListExtra(EXTRA_DATA) ?: arrayListOf()
id = intent.getStringExtra(EXTRA_ID) ?: UUID.randomUUID().toString()
if (!intent.hasExtra(EXTRA_ID)) {
Log.d(TAG, "Created new intent cache with id $id")
} else if (intent.hasExtra(EXTRA_DATA)) {
Log.d(TAG, "Recovered data from intent cache with id $id")
}
pendingActions.forEach { it() }
pendingActions.clear()
isReady = true
updateIntent()
}
}
}
private fun runIfReady(action: () -> Unit) {
synchronized(lock) {
if (isReady) {
action()
} else {
pendingActions.add(action)
}
}
}
private fun updateIntent() {
synchronized(lock) {
if (isReady) {
val intent = getIntent().apply {
putExtra(EXTRA_ID, id)
putParcelableArrayListExtra(EXTRA_DATA, content)
}
val pendingIntent = PendingIntentCompat.getService(context, type, intent, FLAG_NO_CREATE or FLAG_UPDATE_CURRENT, true)
if (pendingIntent == null) {
Log.w(TAG, "Failed to update existing pending intent, will likely have a loss of information")
}
}
}
}
companion object {
private const val TAG = "IntentCacheManager"
private const val TEN_YEARS = 315360000000L
private const val ACTION = "org.microg.gms.ACTION_INTENT_CACHE_MANAGER"
private const val EXTRA_IS_CACHE = "org.microg.gms.IntentCacheManager.is_cache"
private const val EXTRA_CACHE_TYPE = "org.microg.gms.IntentCacheManager.cache_type"
private const val EXTRA_ID = "org.microg.gms.IntentCacheManager.id"
private const val EXTRA_DATA = "org.microg.gms.IntentCacheManager.data"
inline fun<reified S: Service, T: Parcelable> create(context: Context, type: Int) = IntentCacheManager<S, T>(context, S::class.java, type)
fun isCache(intent: Intent): Boolean = try {
intent.getBooleanExtra(EXTRA_IS_CACHE, false)
} catch (e: Exception) {
false
}
fun getType(intent: Intent): Int {
val ret = intent.getIntExtra(EXTRA_CACHE_TYPE, -1)
if (ret == -1) throw IllegalArgumentException()
return ret
}
}
}

View file

@ -0,0 +1,51 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.utils
import android.content.pm.PackageManager
import android.content.pm.PackageManager.NameNotFoundException
import android.content.pm.Signature
import android.util.Base64
import com.google.android.gms.common.internal.CertData
import java.security.MessageDigest
import java.util.*
fun PackageManager.isPlatformCertificate(cert: CertData) = getPlatformCertificates().contains(cert)
fun PackageManager.getPlatformCertificates() = getCertificates("android")
fun PackageManager.getCertificates(packageName: String): List<CertData> = try {
getPackageInfo(packageName, PackageManager.GET_SIGNATURES).signatures?.map { CertData(it.toByteArray()) }
?: emptyList()
} catch (e: NameNotFoundException) {
emptyList()
}
@Deprecated("It's actually a certificate", ReplaceWith("getCertificates"))
fun PackageManager.getSignatures(packageName: String): Array<Signature> = try {
getPackageInfo(packageName, PackageManager.GET_SIGNATURES).signatures
?: emptyArray()
} catch (e: NameNotFoundException) {
emptyArray()
}
fun PackageManager.getApplicationLabel(packageName: String): CharSequence = try {
getApplicationLabel(getApplicationInfo(packageName, 0))
} catch (e: Exception) {
packageName
}
fun PackageManager.getExtendedPackageInfo(packageName: String) = ExtendedPackageInfo(this, packageName)
fun ByteArray.toBase64(vararg flags: Int): String = Base64.encodeToString(this, flags.fold(0) { a, b -> a or b })
fun ByteArray.toHexString(separator: String = ""): String = joinToString(separator) { "%02x".format(it) }
fun PackageManager.getFirstSignatureDigest(packageName: String, md: String): ByteArray? =
getCertificates(packageName).firstOrNull()?.digest(md)
fun ByteArray.digest(md: String): ByteArray = MessageDigest.getInstance(md).digest(this)
@Deprecated("It's actually a certificate")
fun Signature.digest(md: String): ByteArray = toByteArray().digest(md)
fun CertData.digest(md: String): ByteArray = bytes.digest(md)

View file

@ -0,0 +1,529 @@
/*
* SPDX-FileCopyrightText: 2021, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.utils
import android.annotation.TargetApi
import android.content.ComponentName
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.*
import android.content.res.Resources
import android.content.res.XmlResourceParser
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.UserHandle
import androidx.annotation.RequiresApi
open class PackageManagerWrapper(private val wrapped: PackageManager) : PackageManager() {
override fun getPackageInfo(packageName: String, flags: Int): PackageInfo {
return wrapped.getPackageInfo(packageName, flags)
}
@TargetApi(26)
override fun getPackageInfo(versionedPackage: VersionedPackage, flags: Int): PackageInfo {
return wrapped.getPackageInfo(versionedPackage, flags)
}
override fun currentToCanonicalPackageNames(packageNames: Array<out String>): Array<String> {
return wrapped.currentToCanonicalPackageNames(packageNames)
}
override fun canonicalToCurrentPackageNames(packageNames: Array<out String>): Array<String> {
return wrapped.canonicalToCurrentPackageNames(packageNames)
}
override fun getLaunchIntentForPackage(packageName: String): Intent? {
return wrapped.getLaunchIntentForPackage(packageName)
}
@TargetApi(21)
override fun getLeanbackLaunchIntentForPackage(packageName: String): Intent? {
return wrapped.getLeanbackLaunchIntentForPackage(packageName)
}
override fun getPackageGids(packageName: String): IntArray {
return wrapped.getPackageGids(packageName)
}
@TargetApi(24)
override fun getPackageGids(packageName: String, flags: Int): IntArray {
return wrapped.getPackageGids(packageName, flags)
}
@TargetApi(24)
override fun getPackageUid(packageName: String, flags: Int): Int {
return wrapped.getPackageUid(packageName, flags)
}
override fun getPermissionInfo(permName: String, flags: Int): PermissionInfo {
return wrapped.getPermissionInfo(permName, flags)
}
override fun queryPermissionsByGroup(permissionGroup: String?, flags: Int): MutableList<PermissionInfo> {
return wrapped.queryPermissionsByGroup(permissionGroup, flags)
}
override fun getPermissionGroupInfo(permName: String, flags: Int): PermissionGroupInfo {
return wrapped.getPermissionGroupInfo(permName, flags)
}
override fun getAllPermissionGroups(flags: Int): MutableList<PermissionGroupInfo> {
return wrapped.getAllPermissionGroups(flags)
}
override fun getApplicationInfo(packageName: String, flags: Int): ApplicationInfo {
return wrapped.getApplicationInfo(packageName, flags)
}
override fun getActivityInfo(component: ComponentName, flags: Int): ActivityInfo {
return wrapped.getActivityInfo(component, flags)
}
override fun getReceiverInfo(component: ComponentName, flags: Int): ActivityInfo {
return wrapped.getReceiverInfo(component, flags)
}
override fun getServiceInfo(component: ComponentName, flags: Int): ServiceInfo {
return wrapped.getServiceInfo(component, flags)
}
override fun getProviderInfo(component: ComponentName, flags: Int): ProviderInfo {
return wrapped.getProviderInfo(component, flags)
}
@RequiresApi(29)
override fun getInstalledModules(flags: Int): MutableList<ModuleInfo> {
return wrapped.getInstalledModules(flags)
}
override fun getInstalledPackages(flags: Int): MutableList<PackageInfo> {
return wrapped.getInstalledPackages(flags)
}
@TargetApi(18)
override fun getPackagesHoldingPermissions(permissions: Array<out String>, flags: Int): MutableList<PackageInfo> {
return wrapped.getPackagesHoldingPermissions(permissions, flags)
}
override fun checkPermission(permName: String, packageName: String): Int {
return wrapped.checkPermission(permName, packageName)
}
@TargetApi(23)
override fun isPermissionRevokedByPolicy(permName: String, packageName: String): Boolean {
return wrapped.isPermissionRevokedByPolicy(permName, packageName)
}
override fun addPermission(info: PermissionInfo): Boolean {
return wrapped.addPermission(info)
}
override fun addPermissionAsync(info: PermissionInfo): Boolean {
return wrapped.addPermissionAsync(info)
}
override fun removePermission(permName: String) {
return wrapped.removePermission(permName)
}
override fun checkSignatures(packageName1: String, packageName2: String): Int {
return wrapped.checkSignatures(packageName1, packageName2)
}
override fun checkSignatures(uid1: Int, uid2: Int): Int {
return wrapped.checkSignatures(uid1, uid2)
}
override fun getPackagesForUid(uid: Int): Array<String>? {
return wrapped.getPackagesForUid(uid)
}
override fun getNameForUid(uid: Int): String? {
return wrapped.getNameForUid(uid)
}
override fun getInstalledApplications(flags: Int): MutableList<ApplicationInfo> {
return wrapped.getInstalledApplications(flags)
}
@TargetApi(26)
override fun isInstantApp(): Boolean {
return wrapped.isInstantApp
}
@TargetApi(26)
override fun isInstantApp(packageName: String): Boolean {
return wrapped.isInstantApp(packageName)
}
@TargetApi(26)
override fun getInstantAppCookieMaxBytes(): Int {
return wrapped.instantAppCookieMaxBytes
}
@TargetApi(26)
override fun getInstantAppCookie(): ByteArray {
return wrapped.instantAppCookie
}
@TargetApi(26)
override fun clearInstantAppCookie() {
return wrapped.clearInstantAppCookie()
}
@TargetApi(26)
override fun updateInstantAppCookie(cookie: ByteArray?) {
return wrapped.updateInstantAppCookie(cookie)
}
@TargetApi(26)
override fun getSystemSharedLibraryNames(): Array<String>? {
return wrapped.systemSharedLibraryNames
}
@TargetApi(26)
override fun getSharedLibraries(flags: Int): MutableList<SharedLibraryInfo> {
return wrapped.getSharedLibraries(flags)
}
@TargetApi(26)
override fun getChangedPackages(sequenceNumber: Int): ChangedPackages? {
return wrapped.getChangedPackages(sequenceNumber)
}
override fun getSystemAvailableFeatures(): Array<FeatureInfo> {
return wrapped.systemAvailableFeatures
}
override fun hasSystemFeature(featureName: String): Boolean {
return wrapped.hasSystemFeature(featureName)
}
@TargetApi(24)
override fun hasSystemFeature(featureName: String, version: Int): Boolean {
return wrapped.hasSystemFeature(featureName, version)
}
override fun resolveActivity(intent: Intent, flags: Int): ResolveInfo? {
return wrapped.resolveActivity(intent, flags)
}
override fun queryIntentActivities(intent: Intent, flags: Int): MutableList<ResolveInfo> {
return wrapped.queryIntentActivities(intent, flags)
}
override fun queryIntentActivityOptions(caller: ComponentName?, specifics: Array<out Intent>?, intent: Intent, flags: Int): MutableList<ResolveInfo> {
return wrapped.queryIntentActivityOptions(caller, specifics, intent, flags)
}
override fun queryBroadcastReceivers(intent: Intent, flags: Int): MutableList<ResolveInfo> {
return wrapped.queryBroadcastReceivers(intent, flags)
}
override fun resolveService(intent: Intent, flags: Int): ResolveInfo? {
return wrapped.resolveService(intent, flags)
}
override fun queryIntentServices(intent: Intent, flags: Int): MutableList<ResolveInfo> {
return wrapped.queryIntentServices(intent, flags)
}
@TargetApi(19)
override fun queryIntentContentProviders(intent: Intent, flags: Int): MutableList<ResolveInfo> {
return wrapped.queryIntentContentProviders(intent, flags)
}
override fun resolveContentProvider(authority: String, flags: Int): ProviderInfo? {
return wrapped.resolveContentProvider(authority, flags)
}
override fun queryContentProviders(processName: String?, uid: Int, flags: Int): MutableList<ProviderInfo> {
return wrapped.queryContentProviders(processName, uid, flags)
}
override fun getInstrumentationInfo(className: ComponentName, flags: Int): InstrumentationInfo {
return wrapped.getInstrumentationInfo(className, flags)
}
override fun queryInstrumentation(targetPackage: String, flags: Int): MutableList<InstrumentationInfo> {
return wrapped.queryInstrumentation(targetPackage, flags)
}
override fun getDrawable(packageName: String, resid: Int, appInfo: ApplicationInfo?): Drawable? {
return wrapped.getDrawable(packageName, resid, appInfo)
}
override fun getActivityIcon(activityName: ComponentName): Drawable {
return wrapped.getActivityIcon(activityName)
}
override fun getActivityIcon(intent: Intent): Drawable {
return wrapped.getActivityIcon(intent)
}
@TargetApi(20)
override fun getActivityBanner(activityName: ComponentName): Drawable? {
return wrapped.getActivityBanner(activityName)
}
@TargetApi(20)
override fun getActivityBanner(intent: Intent): Drawable? {
return wrapped.getActivityBanner(intent)
}
override fun getDefaultActivityIcon(): Drawable {
return wrapped.defaultActivityIcon
}
override fun getApplicationIcon(info: ApplicationInfo): Drawable {
return wrapped.getApplicationIcon(info)
}
override fun getApplicationIcon(packageName: String): Drawable {
return wrapped.getApplicationIcon(packageName)
}
@TargetApi(20)
override fun getApplicationBanner(info: ApplicationInfo): Drawable? {
return wrapped.getApplicationBanner(info)
}
@TargetApi(20)
override fun getApplicationBanner(packageName: String): Drawable? {
return wrapped.getApplicationBanner(packageName)
}
override fun getActivityLogo(activityName: ComponentName): Drawable? {
return wrapped.getActivityLogo(activityName)
}
override fun getActivityLogo(intent: Intent): Drawable? {
return wrapped.getActivityLogo(intent)
}
override fun getApplicationLogo(info: ApplicationInfo): Drawable? {
return wrapped.getApplicationLogo(info)
}
override fun getApplicationLogo(packageName: String): Drawable? {
return wrapped.getApplicationLogo(packageName)
}
@TargetApi(21)
override fun getUserBadgedIcon(drawable: Drawable, user: UserHandle): Drawable {
return wrapped.getUserBadgedIcon(drawable, user)
}
@TargetApi(21)
override fun getUserBadgedDrawableForDensity(drawable: Drawable, user: UserHandle, badgeLocation: Rect?, badgeDensity: Int): Drawable {
return wrapped.getUserBadgedDrawableForDensity(drawable, user, badgeLocation, badgeDensity)
}
@TargetApi(21)
override fun getUserBadgedLabel(label: CharSequence, user: UserHandle): CharSequence {
return wrapped.getUserBadgedLabel(label, user)
}
override fun getText(packageName: String, resid: Int, appInfo: ApplicationInfo?): CharSequence? {
return wrapped.getText(packageName, resid, appInfo)
}
override fun getXml(packageName: String, resid: Int, appInfo: ApplicationInfo?): XmlResourceParser? {
return wrapped.getXml(packageName, resid, appInfo)
}
override fun getApplicationLabel(info: ApplicationInfo): CharSequence {
return wrapped.getApplicationLabel(info)
}
override fun getResourcesForActivity(activityName: ComponentName): Resources {
return wrapped.getResourcesForActivity(activityName)
}
override fun getResourcesForApplication(app: ApplicationInfo): Resources {
return wrapped.getResourcesForApplication(app)
}
override fun getResourcesForApplication(packageName: String): Resources {
return wrapped.getResourcesForApplication(packageName)
}
override fun verifyPendingInstall(id: Int, verificationCode: Int) {
return wrapped.verifyPendingInstall(id, verificationCode)
}
@TargetApi(17)
override fun extendVerificationTimeout(id: Int, verificationCodeAtTimeout: Int, millisecondsToDelay: Long) {
return wrapped.extendVerificationTimeout(id, verificationCodeAtTimeout, millisecondsToDelay)
}
override fun setInstallerPackageName(targetPackage: String, installerPackageName: String?) {
return wrapped.setInstallerPackageName(targetPackage, installerPackageName)
}
override fun getInstallerPackageName(packageName: String): String? {
return wrapped.getInstallerPackageName(packageName)
}
override fun addPackageToPreferred(packageName: String) {
return wrapped.addPackageToPreferred(packageName)
}
override fun removePackageFromPreferred(packageName: String) {
return wrapped.removePackageFromPreferred(packageName)
}
override fun getPreferredPackages(flags: Int): MutableList<PackageInfo> {
return wrapped.getPreferredPackages(flags)
}
override fun addPreferredActivity(filter: IntentFilter, match: Int, set: Array<out ComponentName>?, activity: ComponentName) {
return wrapped.addPreferredActivity(filter, match, set, activity)
}
override fun clearPackagePreferredActivities(packageName: String) {
return wrapped.clearPackagePreferredActivities(packageName)
}
override fun getPreferredActivities(outFilters: MutableList<IntentFilter>, outActivities: MutableList<ComponentName>, packageName: String?): Int {
return wrapped.getPreferredActivities(outFilters, outActivities, packageName)
}
override fun setComponentEnabledSetting(componentName: ComponentName, newState: Int, flags: Int) {
return wrapped.setComponentEnabledSetting(componentName, newState, flags)
}
override fun getComponentEnabledSetting(componentName: ComponentName): Int {
return wrapped.getComponentEnabledSetting(componentName)
}
override fun setApplicationEnabledSetting(packageName: String, newState: Int, flags: Int) {
return wrapped.setApplicationEnabledSetting(packageName, newState, flags)
}
override fun getApplicationEnabledSetting(packageName: String): Int {
return wrapped.getApplicationEnabledSetting(packageName)
}
override fun isSafeMode(): Boolean {
return wrapped.isSafeMode
}
@TargetApi(26)
override fun setApplicationCategoryHint(packageName: String, categoryHint: Int) {
return wrapped.setApplicationCategoryHint(packageName, categoryHint)
}
@TargetApi(21)
override fun getPackageInstaller(): PackageInstaller {
return wrapped.packageInstaller
}
@TargetApi(26)
override fun canRequestPackageInstalls(): Boolean {
return wrapped.canRequestPackageInstalls()
}
@TargetApi(29)
override fun addWhitelistedRestrictedPermission(packageName: String, permName: String, whitelistFlags: Int): Boolean {
return wrapped.addWhitelistedRestrictedPermission(packageName, permName, whitelistFlags)
}
@TargetApi(30)
override fun getBackgroundPermissionOptionLabel(): CharSequence {
return wrapped.getBackgroundPermissionOptionLabel()
}
@TargetApi(30)
override fun getInstallSourceInfo(packageName: String): InstallSourceInfo {
return wrapped.getInstallSourceInfo(packageName)
}
@TargetApi(30)
override fun getMimeGroup(mimeGroup: String): MutableSet<String> {
return wrapped.getMimeGroup(mimeGroup)
}
@TargetApi(29)
override fun getModuleInfo(packageName: String, flags: Int): ModuleInfo {
return wrapped.getModuleInfo(packageName, flags)
}
override fun getPackageArchiveInfo(archiveFilePath: String, flags: Int): PackageInfo? {
return wrapped.getPackageArchiveInfo(archiveFilePath, flags)
}
@TargetApi(28)
override fun getSuspendedPackageAppExtras(): Bundle? {
return wrapped.suspendedPackageAppExtras
}
@TargetApi(29)
override fun getSyntheticAppDetailsActivityEnabled(packageName: String): Boolean {
return wrapped.getSyntheticAppDetailsActivityEnabled(packageName)
}
@TargetApi(29)
override fun getWhitelistedRestrictedPermissions(packageName: String, whitelistFlag: Int): MutableSet<String> {
return wrapped.getWhitelistedRestrictedPermissions(packageName, whitelistFlag)
}
@TargetApi(28)
override fun hasSigningCertificate(packageName: String, certificate: ByteArray, type: Int): Boolean {
return wrapped.hasSigningCertificate(packageName, certificate, type)
}
@TargetApi(28)
override fun hasSigningCertificate(uid: Int, certificate: ByteArray, type: Int): Boolean {
return wrapped.hasSigningCertificate(uid, certificate, type)
}
@TargetApi(30)
override fun isAutoRevokeWhitelisted(): Boolean {
return wrapped.isAutoRevokeWhitelisted
}
@TargetApi(30)
override fun isAutoRevokeWhitelisted(packageName: String): Boolean {
return wrapped.isAutoRevokeWhitelisted(packageName)
}
@TargetApi(30)
override fun isDefaultApplicationIcon(drawable: Drawable): Boolean {
return wrapped.isDefaultApplicationIcon(drawable)
}
@TargetApi(29)
override fun isDeviceUpgrading(): Boolean {
return wrapped.isDeviceUpgrading
}
@TargetApi(28)
override fun isPackageSuspended(): Boolean {
return wrapped.isPackageSuspended
}
@TargetApi(29)
override fun isPackageSuspended(packageName: String): Boolean {
return wrapped.isPackageSuspended(packageName)
}
@TargetApi(29)
override fun removeWhitelistedRestrictedPermission(packageName: String, permName: String, whitelistFlags: Int): Boolean {
return wrapped.removeWhitelistedRestrictedPermission(packageName, permName, whitelistFlags)
}
@TargetApi(30)
override fun setAutoRevokeWhitelisted(packageName: String, whitelisted: Boolean): Boolean {
return wrapped.setAutoRevokeWhitelisted(packageName, whitelisted)
}
@TargetApi(30)
override fun setMimeGroup(mimeGroup: String, mimeTypes: MutableSet<String>) {
return wrapped.setMimeGroup(mimeGroup, mimeTypes)
}
}

View file

@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.utils
private val singleInstanceLock = Any()
private val singleInstanceMap: MutableMap<Class<*>, Any> = hashMapOf()
fun <T : Any> singleInstanceOf(tClass: Class<T>, tCreator: () -> T): T {
val tVolatileItem = singleInstanceMap[tClass]
@Suppress("UNCHECKED_CAST")
if (tVolatileItem != null && tClass.isAssignableFrom(tVolatileItem.javaClass)) return tVolatileItem as T
val tLock = synchronized(singleInstanceLock) {
val tItem = singleInstanceMap[tClass]
if (tItem != null) {
@Suppress("UNCHECKED_CAST")
if (tClass.isAssignableFrom(tItem.javaClass)) return tItem as T
tItem
} else {
val tLock = Any()
singleInstanceMap[tClass] = tLock
tLock
}
}
synchronized(tLock) {
val tItem = synchronized(singleInstanceMap) { singleInstanceMap[tClass] }
if (tItem == null) throw IllegalStateException()
@Suppress("UNCHECKED_CAST")
if (tClass.isAssignableFrom(tItem.javaClass)) return tItem as T
if (tItem != tLock) throw IllegalStateException()
val tNewItem = tCreator()
synchronized(singleInstanceMap) {
singleInstanceMap[tClass] = tNewItem
}
return tNewItem
}
}
inline fun <reified T : Any> singleInstanceOf(noinline tCreator: () -> T): T = singleInstanceOf(T::class.java, tCreator)

View file

@ -0,0 +1,61 @@
/**
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.vending
import org.json.JSONException
import org.json.JSONObject
enum class AllowType(val value: Int) {
REJECT_ALWAYS(0),
REJECT_ONCE(1),
ALLOW_ONCE(2),
ALLOW_ALWAYS(3),
}
data class InstallerData(val packageName: String, var allowType: Int, val pkgSignSha256: String) {
override fun toString(): String {
return JSONObject()
.put(CHANNEL_PACKAGE_NAME, packageName)
.put(CHANNEL_ALLOW_TYPE, allowType)
.put(CHANNEL_SIGNATURE, pkgSignSha256)
.toString()
}
companion object {
private const val CHANNEL_PACKAGE_NAME = "packageName"
private const val CHANNEL_ALLOW_TYPE = "allowType"
private const val CHANNEL_SIGNATURE = "signature"
private fun parse(jsonString: String): InstallerData? {
try {
val json = JSONObject(jsonString)
return InstallerData(
json.getString(CHANNEL_PACKAGE_NAME),
json.getInt(CHANNEL_ALLOW_TYPE),
json.getString(CHANNEL_SIGNATURE)
)
} catch (e: JSONException) {
return null
}
}
fun loadDataSet(content: String): Set<InstallerData> {
return content.split("|").mapNotNull { parse(it) }.toSet()
}
fun updateDataSetString(channelList: Set<InstallerData>, channel: InstallerData): String {
val channelData = channelList.find { it.packageName == channel.packageName && it.pkgSignSha256 == channel.pkgSignSha256 }
val newChannelList = if (channelData != null) {
channelData.allowType = channel.allowType
channelList
} else {
channelList + channel
}
return newChannelList.let { it -> it.joinToString(separator = "|") { it.toString() } }
}
}
}

View file

@ -0,0 +1,9 @@
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<vector android:height="24dp" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000000" android:pathData="M19.51,3.08L3.08,19.51c0.09,0.34 0.27,0.65 0.51,0.9 0.25,0.24 0.56,0.42 0.9,0.51L20.93,4.49c-0.19,-0.69 -0.73,-1.23 -1.42,-1.41zM11.88,3L3,11.88v2.83L14.71,3h-2.83zM5,3c-1.1,0 -2,0.9 -2,2v2l4,-4L5,3zM19,21c0.55,0 1.05,-0.22 1.41,-0.59 0.37,-0.36 0.59,-0.86 0.59,-1.41v-2l-4,4h2zM9.29,21h2.83L21,12.12L21,9.29L9.29,21z"/>
</vector>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2019, The Android Open Source Project
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#000"
android:pathData="M9.71,18.71l-1.42,-1.42l5.3,-5.29l-5.3,-5.29l1.42,-1.42l6.7,6.71z" />
</vector>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2019, The Android Open Source Project
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#000"
android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z" />
</vector>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2019, The Android Open Source Project
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z" />
</vector>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2021 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/on"
android:drawable="@drawable/ic_radio_checked"
android:state_checked="true" />
<item
android:id="@+id/off"
android:drawable="@drawable/ic_radio_unchecked"
android:state_checked="false" />
</selector>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2019 The Android Open Source Project
~ SPDX-FileCopyrightText: 2021 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorAccent"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5 5,-2.24 5,-5 -2.24,-5 -5,-5zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" />
</vector>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2019 The Android Open Source Project
~ SPDX-FileCopyrightText: 2021 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" />
</vector>

View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2013 The Android Open Source Project
~ 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.
-->
<!-- text that appears when the recent app list is empty -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:gravity="center_vertical"
android:paddingEnd="?android:attr/scrollbarSize"
android:background="?attr/selectableItemBackground"
android:paddingRight="?android:attr/scrollbarSize">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="15dip"
android:layout_marginEnd="6dip"
android:layout_marginTop="6dip"
android:layout_marginBottom="6dip"
android:layout_weight="1"
android:layout_marginLeft="15dip"
android:layout_marginRight="6dip">
<TextView android:id="@android:id/title"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary" />
</RelativeLayout>
</LinearLayout>

View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ SPDX-FileCopyrightText: 2023 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="?attr/listPreferredItemPaddingStart"
android:paddingLeft="?attr/listPreferredItemPaddingLeft"
android:paddingTop="24dp"
android:paddingEnd="?attr/listPreferredItemPaddingEnd"
android:paddingRight="?attr/listPreferredItemPaddingRight"
android:paddingBottom="16dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:gravity="center_horizontal"
android:orientation="vertical">
<ImageView
android:id="@android:id/icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:antialias="true"
android:scaleType="fitCenter"
tools:src="@android:mipmap/sym_def_app_icon" />
<TextView
android:id="@android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:ellipsize="marquee"
android:gravity="center"
android:singleLine="false"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
android:textColor="?android:attr/textColorPrimary"
android:textSize="20sp"
tools:text="@tools:sample/lorem" />
</LinearLayout>
</RelativeLayout>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<View xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="0dp" />

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ SPDX-FileCopyrightText: 2022 The Android Open Source Project
~ SPDX-FileCopyrightText: 2024 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:background="?android:attr/selectableItemBackground"
android:orientation="vertical"
android:clipToPadding="false">
<LinearLayout
android:id="@+id/icon_frame"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="56dp"
android:gravity="start|top"
android:orientation="horizontal"
android:paddingEnd="12dp"
android:paddingTop="16dp"
android:paddingBottom="4dp">
<ImageView
android:id="@android:id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:textAlignment="viewStart"
android:paddingTop="16dp"
android:paddingBottom="8dp"
android:textColor="?android:attr/textColorSecondary"
android:hyphenationFrequency="normalFast"
android:lineBreakWordStyle="phrase"
android:ellipsize="marquee"/>
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ProgressBar
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true" />
</LinearLayout>

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ SPDX-FileCopyrightText: 2020 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:gravity="center"
android:orientation="horizontal"
android:paddingStart="?attr/listPreferredItemPaddingStart"
android:paddingLeft="?attr/listPreferredItemPaddingLeft"
android:paddingEnd="?attr/listPreferredItemPaddingEnd"
android:paddingRight="?attr/listPreferredItemPaddingRight"
tools:background="?attr/colorControlActivated">
<TextView
android:id="@android:id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="2"
android:paddingStart="56dp"
android:paddingLeft="56dp"
android:textAppearance="?attr/textAppearanceListItem"
android:textColor="@android:color/white"
tools:text="Enabled" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_widget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:background="@null"
app:thumbTint="@android:color/white"
app:trackTint="@android:color/darker_gray"
tools:checked="true" />
</LinearLayout>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="menu_advanced">إعدادات متقدمة</string>
<string name="list_no_item_none">لا يوجد</string>
<string name="list_item_see_all">عرض الكل</string>
<string name="open_app">فتح</string>
<string name="service_status_disabled">غير مفعّل</string>
<string name="service_status_enabled">مفعّل</string>
<string name="service_status_automatic">تلقائي</string>
<string name="service_status_manual">يدوي</string>
<string name="service_status_enabled_short">مفعّل</string>
<string name="service_status_disabled_short">غير مفعّل</string>
<string name="foreground_service_notification_title">نشط في الخلفية</string>
<string name="foreground_service_notification_big_text">استثني &lt;xliff:g example=\"microG Services\"&gt;%1$s&lt;/xliff:g&gt; من توفير شحن البطارية أو غير إعدادات الإشعارات لإخفاء هذا اﻹشعار.</string>
<string name="foreground_service_notification_text">&lt;xliff:g example=\"Exposure Notification\"&gt;%1$s&lt;/xliff:g&gt; يعمل في الخلفية.</string>
</resources>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="menu_advanced">Configuración avanzada</string>
<string name="service_status_enabled_short"></string>
<string name="open_app">Abrir</string>
<string name="foreground_service_notification_text">«<xliff:g example="Exposure Notification">%1$s</xliff:g>» ta executándose en segundu planu.</string>
<string name="foreground_service_notification_big_text">Esclúi a <xliff:g example="microG Services">%1$s</xliff:g> de les optimizaciones d\'enerxía o camuda la configuración de los avisos pa esconder esti avisu.</string>
<string name="list_item_see_all">Ver too</string>
<string name="service_status_disabled_short">Non</string>
<string name="list_no_item_none">Nada</string>
<string name="service_status_manual">Manual</string>
</resources>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="foreground_service_notification_title">Arxa planda işləkdir</string>
<string name="foreground_service_notification_big_text">Batareya optimallaşmasından <xliff:g example="microG Services"> %1$s</xliff:g>-i çıxar və ya bu bildirişi gizlətmək üçün bildiriş seçimlərin dəyişdir.</string>
<string name="menu_advanced">Qabaqcıl</string>
<string name="list_item_see_all">Hamısın gör</string>
<string name="open_app"></string>
<string name="service_status_disabled">Qeyri-aktiv</string>
<string name="service_status_enabled">Aktivdir</string>
<string name="service_status_automatic">Avtomatik</string>
<string name="service_status_manual">Əl ilə</string>
<string name="service_status_enabled_short">Aktiv</string>
<string name="service_status_disabled_short">Bağlı</string>
<string name="list_no_item_none">Heç biri</string>
</resources>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="foreground_service_notification_title">Фонавая актыўнасць</string>
<string name="foreground_service_notification_text"><xliff:g example="Exposure Notification">%1$s</xliff:g> працуе ў фонавым рэжыме.</string>
<string name="foreground_service_notification_big_text">Адключыце эканомію выкарыстання акумулятара для <xliff:g example="microG Services">%1$s</xliff:g>, каб ўбраць гэтае паведамленне.</string>
<string name="menu_advanced">Дадаткова</string>
<string name="list_no_item_none">Пуста</string>
<string name="list_item_see_all">Паказаць усё</string>
<string name="open_app">Aдкрыць</string>
<string name="service_status_disabled">Выключана</string>
<string name="service_status_enabled">Уключана</string>
<string name="service_status_automatic">Аўтаматычна</string>
<string name="service_status_manual">Уручную</string>
<string name="service_status_enabled_short">Укл.</string>
<string name="service_status_disabled_short">Выкл.</string>
</resources>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="foreground_service_notification_title">Aktivní na pozadí</string>
<string name="foreground_service_notification_text"><xliff:g example="Oznámení o možném kontaktu">%1$s</xliff:g> běží na pozadí.</string>
<string name="foreground_service_notification_big_text">Pro skrytí tohoto oznámení vypněte optimalizaci baterie pro <xliff:g example="Služby microG">%1$s</xliff:g> nebo změňte nastavení oznámení.</string>
<string name="list_no_item_none">Žádné</string>
<string name="list_item_see_all">Zobrazit vše</string>
<string name="open_app">Otevřít</string>
<string name="service_status_disabled">Zakázáno</string>
<string name="service_status_enabled">Povoleno</string>
<string name="service_status_enabled_short">Zap</string>
<string name="menu_advanced">Pokročilé</string>
<string name="service_status_automatic">Automatické</string>
<string name="service_status_manual">Ruční</string>
<string name="service_status_disabled_short">Vyp</string>
<string name="menu_game_managed">Spravované herní účty</string>
</resources>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
--><resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="foreground_service_notification_title">Im Hintergrund aktiv</string>
<string name="foreground_service_notification_text"><xliff:g example="Exposure Notification">%1$s</xliff:g> läuft im Hintergrund weiter.</string>
<string name="foreground_service_notification_big_text">Füge <xliff:g example="microG Services">%1$s</xliff:g> als Ausnahme zur Batterie-Optimierung hinzu oder verstecke diese Benachrichtigung in den Systemeinstelleungen.</string>
<string name="menu_advanced">Erweitert</string>
<string name="list_no_item_none">Keine</string>
<string name="list_item_see_all">Alle anzeigen</string>
<string name="open_app">Öffnen</string>
<string name="service_status_disabled">Deaktiviert</string>
<string name="service_status_enabled">Aktiviert</string>
<string name="service_status_automatic">Automatisch</string>
<string name="service_status_manual">Manuell</string>
<string name="service_status_enabled_short">Ein</string>
<string name="service_status_disabled_short">Aus</string>
<string name="menu_game_managed">Verwaltete Spielkonten</string>
</resources>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
--><resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="menu_advanced">Avanzado</string>
<string name="list_no_item_none">Ninguno</string>
<string name="list_item_see_all">Ver todo</string>
<string name="open_app">Abrir</string>
<string name="service_status_disabled">Desactivado</string>
<string name="service_status_enabled">Activado</string>
<string name="service_status_automatic">Automático</string>
<string name="service_status_manual">Manual</string>
<string name="service_status_enabled_short">Encendido</string>
<string name="service_status_disabled_short">Apagado</string>
<string name="foreground_service_notification_title">Activo en segundo plano</string>
<string name="foreground_service_notification_text"><xliff:g example="Exposure Notification">%1$s</xliff:g> se está ejecutando en segundo plano.</string>
<string name="foreground_service_notification_big_text">Excluye <xliff:g example="microG Services">%1$s</xliff:g> de las optimizaciones de la batería o cambia la configuración de las notificaciones para ocultar esta notificación.</string>
</resources>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="foreground_service_notification_text"><xliff:g example="آگاه‌سازهای نزدیکی">%1$s</xliff:g> در پس زمینه اجرا می شود.</string>
<string name="service_status_manual">دستی</string>
<string name="menu_advanced">پیشرفته</string>
<string name="list_no_item_none">هیچکدام</string>
<string name="list_item_see_all">دیدن همه</string>
<string name="foreground_service_notification_title">در پس‌زمینه روشن است</string>
<string name="open_app">بازکردن</string>
<string name="service_status_disabled">غیرفعال</string>
<string name="service_status_enabled">فعال</string>
<string name="service_status_automatic">خودکار</string>
<string name="service_status_disabled_short">خاموش</string>
<string name="service_status_enabled_short">روشن</string>
<string name="foreground_service_notification_big_text"><xliff:g example="خدمات میکروجی">%1$s</xliff:g> را از بهینه‌سازی باتری پاک کنید یا تنظیمات آگاه‌ساز را تغییر دهید تا این آگاه‌ساز پنهان شود.</string>
<string name="menu_game_managed">اکانت های بازی مدیریت شد</string>
</resources>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="service_status_automatic">Automaattinen</string>
<string name="open_app">Avaa</string>
<string name="service_status_disabled">Poistettu käytöstä</string>
<string name="service_status_enabled">Käytössä</string>
<string name="list_item_see_all">Näytä kaikki</string>
<string name="list_no_item_none">Ei mitään</string>
<string name="service_status_manual">Manuaalinen</string>
<string name="service_status_enabled_short">Päällä</string>
<string name="service_status_disabled_short">Pois päältä</string>
<string name="foreground_service_notification_title">Käynnissä taustalla</string>
</resources>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="menu_advanced">Advanced</string>
<string name="list_no_item_none">Wala</string>
<string name="list_item_see_all">Tignan lahat</string>
<string name="open_app">Buksan</string>
<string name="service_status_disabled">Naka-disable</string>
<string name="service_status_enabled">Naka-enable</string>
<string name="service_status_automatic">Awtomatiko</string>
<string name="service_status_manual">Manwal</string>
<string name="service_status_enabled_short">Nakabukas</string>
<string name="service_status_disabled_short">Nakapatay</string>
<string name="foreground_service_notification_title">Aktibo sa background</string>
<string name="foreground_service_notification_text">Tumatakbo ang <xliff:g example="Exposure Notification">%1$s</xliff:g> sa background.</string>
<string name="foreground_service_notification_big_text">Ibukod ang <xliff:g example="microG Services">%1$s</xliff:g> sa pag-optimize ng baterya o palitan ang mga setting ng notification para itago ang notification na ito.</string>
<string name="menu_game_managed">Mga Pinamamahalaang Game Account</string>
</resources>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
--><resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="menu_advanced">Avancé</string>
<string name="list_no_item_none">Aucun</string>
<string name="service_status_disabled">Désactivé</string>
<string name="service_status_enabled">Activé</string>
<string name="service_status_automatic">Automatique</string>
<string name="service_status_manual">Manuel</string>
<string name="foreground_service_notification_title">Actif en arrière-plan</string>
<string name="open_app">Ouvrir</string>
<string name="foreground_service_notification_big_text">Exclure <xliff:g example="microG Services">%1$s</xliff:g> de l\'optimisation de la batterie ou modifier les paramètres des notifications pour désactiver cette notification.</string>
<string name="service_status_enabled_short">Activé</string>
<string name="service_status_disabled_short">Désactivé</string>
<string name="list_item_see_all">Tout voir</string>
<string name="foreground_service_notification_text"><xliff:g example="Exposure Notification">%1$s</xliff:g> fonctionne en arrière-plan.</string>
<string name="menu_game_managed">Comptes de Jeux Gérés</string>
</resources>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="foreground_service_notification_title">Gníomhach sa chúlra</string>
<string name="menu_advanced">Casta</string>
<string name="service_status_enabled">Cumasaithe</string>
<string name="service_status_automatic">Uathoibríoch</string>
<string name="service_status_manual">Lámhleabhar</string>
<string name="foreground_service_notification_big_text">Fág <xliff:g example="MicroG Services">%1$s</xliff:g> as an mbarrfheabhsú ceallraí nó athraigh socruithe fógra chun an fógra seo a chur i bhfolach.</string>
<string name="list_no_item_none">Dada</string>
<string name="foreground_service_notification_text"><xliff:g example="Fógra Nochta">%1$s</xliff:g> ag rith sa chúlra.</string>
<string name="list_item_see_all">Féach ar fad</string>
<string name="service_status_enabled_short">Ar</string>
<string name="open_app">Oscail</string>
<string name="service_status_disabled">Faoi mhíchumas</string>
<string name="service_status_disabled_short">As</string>
<string name="menu_game_managed">Cuntais Cluiche Bainistithe</string>
</resources>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="list_item_see_all">Lihat semua</string>
<string name="service_status_disabled">Dinonaktifkan</string>
<string name="service_status_enabled">Diaktifkan</string>
<string name="service_status_automatic">Otomatis</string>
<string name="service_status_manual">Manual</string>
<string name="service_status_enabled_short">Aktif</string>
<string name="service_status_disabled_short">Tidak aktif</string>
<string name="foreground_service_notification_title">Aktif di latar belakang</string>
<string name="foreground_service_notification_text"><xliff:g example="Pemberitahuan Paparan">%1$s</xliff:g> berjalan di latar belakang.</string>
<string name="foreground_service_notification_big_text">Exclude <xliff:g example="Layanan microG">%1$s</xliff:g> dari pengoptimalan baterai atau ubah pengaturan notifikasi untuk menyembunyikan notifikasi ini.</string>
<string name="menu_advanced">Lanjutan</string>
<string name="list_no_item_none">Tidak ada</string>
<string name="open_app">Buka</string>
<string name="menu_game_managed">Akun Game yang Dikelola</string>
</resources>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="foreground_service_notification_title">Virkt í bakgrunni</string>
<string name="menu_advanced">Ítarlegt</string>
<string name="list_no_item_none">Ekkert</string>
<string name="foreground_service_notification_text"><xliff:g example="Tilkynning um berskjöldun">%1$s</xliff:g> er keyrandi í bakgrunni.</string>
<string name="foreground_service_notification_big_text">Undanskilja <xliff:g example="microG-þjónustur">%1$s</xliff:g> frá rafhlöðusparnaði eða breyttu stillingum tilkynninga til að fela þessa tilkynningu.</string>
<string name="service_status_enabled">Virkt</string>
<string name="service_status_automatic">Sjálfvirkt</string>
<string name="service_status_disabled_short">Slökkt</string>
<string name="open_app">Opna</string>
<string name="service_status_disabled">Óvirkt</string>
<string name="list_item_see_all">Sjá allt</string>
<string name="service_status_manual">Handvirkt</string>
<string name="service_status_enabled_short">Kveikt</string>
<string name="menu_game_managed">Stjórnaðir leikjareikningar</string>
</resources>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="foreground_service_notification_title">Esecuzione in background</string>
<string name="foreground_service_notification_text"><xliff:g example="Exposure Notification">%1$s</xliff:g> è in esecuzione in background.</string>
<string name="foreground_service_notification_big_text">Disabilita le ottimizzazioni della batteria per <xliff:g example="microG Services">%1$s</xliff:g> oppure modifica le impostazioni delle notifiche per nascondere questa notifica.</string>
<string name="menu_advanced">Impostazioni avanzate</string>
<string name="list_no_item_none">Nessuna</string>
<string name="list_item_see_all">Mostra tutte</string>
<string name="open_app">Apri</string>
<string name="service_status_disabled">Disabilitato</string>
<string name="service_status_enabled">Abilitato</string>
<string name="service_status_automatic">Automatico</string>
<string name="service_status_manual">Manuale</string>
<string name="service_status_enabled_short">Abilitato</string>
<string name="service_status_disabled_short">Disabilitato</string>
<string name="menu_game_managed">Account giochi gestiti</string>
</resources>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
--><resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="menu_advanced">詳細設定</string>
<string name="list_no_item_none">なし</string>
<string name="list_item_see_all">すべて表示</string>
<string name="open_app">開く</string>
<string name="service_status_disabled">無効</string>
<string name="service_status_enabled">有効</string>
<string name="service_status_automatic">自動</string>
<string name="service_status_manual">手動</string>
<string name="service_status_enabled_short">On</string>
<string name="service_status_disabled_short">オフ</string>
<string name="foreground_service_notification_title">バックグラウンドで有効</string>
<string name="foreground_service_notification_text"><xliff:g example="Exposure Notification">%1$s</xliff:g> をバックグラウンドで実行しています。</string>
<string name="foreground_service_notification_big_text"><xliff:g example="microG Services">%1$s</xliff:g> をバッテリー最適化から除外するか、通知設定でこの通知を非表示にしてください。</string>
</resources>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="foreground_service_notification_title">백그라운드에서 활성화됨</string>
<string name="foreground_service_notification_text"><xliff:g example="Exposure Notification">%1$s</xliff:g>이 백그라운드에서 실행 중입니다.</string>
<string name="foreground_service_notification_big_text">이 알림을 숨기려면 <xliff:g example="microG Services">%1$s</xliff:g>를 배터리 최적화 목록에서 제외하거나 알림 설정을 변경하세요.</string>
<string name="menu_advanced">고급</string>
<string name="menu_game_managed">관리된 게임 계정</string>
<string name="list_no_item_none">없음</string>
<string name="list_item_see_all">모두 보기</string>
<string name="open_app">열기</string>
<string name="service_status_disabled">비활성화됨</string>
<string name="service_status_enabled">활성화됨</string>
<string name="service_status_automatic">자동</string>
<string name="service_status_manual">수동</string>
<string name="service_status_enabled_short">켜짐</string>
<string name="service_status_disabled_short">꺼짐</string>
</resources>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="foreground_service_notification_title">പശ്ചാത്തലത്തിൽ സജീവമാണ്</string>
<string name="foreground_service_notification_text"><xliff:g example="Exposure Notification">%1$s</xliff:g> പശ്ചാത്തലത്തിൽ പ്രവർത്തിക്കുന്നു.</string>
<string name="foreground_service_notification_big_text">ബാറ്ററി ഒപ്റ്റിമൈസേഷനുകളിൽ നിന്ന് <xliff:g example="microG Services">%1$s</xliff:g> ഒഴിവാക്കുക അല്ലെങ്കിൽ ഈ അറിയിപ്പ് മറയ്ക്കാൻ അറിയിപ്പ് ക്രമീകരണങ്ങൾ മാറ്റുക.</string>
<string name="menu_advanced">വിപുലമായത്</string>
<string name="menu_game_managed">ഗെയിം അക്കൗണ്ടുകൾ കൈകാര്യം ചെയ്യുന്നു</string>
<string name="list_no_item_none">ഒന്നുമില്ല</string>
<string name="list_item_see_all">എല്ലാം കാണുക</string>
<string name="open_app">തുറക്കുക</string>
<string name="service_status_disabled">അപ്രാപ്തമാക്കി</string>
<string name="service_status_enabled">പ്രവർത്തനക്ഷമമാക്കി</string>
<string name="service_status_automatic">ഓട്ടോമാറ്റിക്</string>
<string name="service_status_manual">മാനുവൽ</string>
<string name="service_status_enabled_short">ഓൺ</string>
<string name="service_status_disabled_short">ഓഫ്</string>
</resources>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="menu_advanced">Avansert</string>
<string name="open_app">Åpne</string>
<string name="foreground_service_notification_title">Aktiv i bakgrunnen</string>
<string name="foreground_service_notification_text"><xliff:g example="Exposure Notification">%1$s</xliff:g> kjører i bakgrunnen.</string>
<string name="foreground_service_notification_big_text">Ekskluder <xliff:g example="microG Services">%1$s</xliff:g> fra batterioptimaliseringer eller endre varselinnstillinger for å gjemme dette varselet.</string>
<string name="menu_game_managed">Håndterte spillkontoer</string>
<string name="list_no_item_none">Ingen</string>
<string name="list_item_see_all">Se alle</string>
<string name="service_status_disabled">Deaktivert</string>
<string name="service_status_enabled">Aktivert</string>
<string name="service_status_automatic">Automatisk</string>
<string name="service_status_manual">Manuelt</string>
<string name="service_status_enabled_short"></string>
<string name="service_status_disabled_short">Av</string>
</resources>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="foreground_service_notification_title">Actief op achtergrond</string>
<string name="foreground_service_notification_text"><xliff:g example="Blootstelling Melding">%1$s</xliff:g> draait op de achtergrond.</string>
<string name="foreground_service_notification_big_text">Sluit &lt;xliff:g example=“microG Services”&gt;%1$s&lt;/xliff:g&gt; uit van batterijoptimalisaties of wijzig de instellingen voor meldingen om deze melding te verbergen.</string>
<string name="menu_advanced">Geavanceerd</string>
<string name="list_no_item_none">Geen</string>
<string name="list_item_see_all">Zie alles</string>
<string name="open_app">Open</string>
<string name="service_status_disabled">Uitgeschakeld</string>
<string name="service_status_enabled">Ingeschakeld</string>
<string name="service_status_automatic">Automatisch</string>
<string name="service_status_manual">Manueel</string>
<string name="service_status_enabled_short">Aan</string>
<string name="service_status_disabled_short">Uit</string>
</resources>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
--><resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="menu_advanced">Zaawansowane</string>
<string name="list_no_item_none">Brak</string>
<string name="service_status_disabled">Wyłączona</string>
<string name="service_status_enabled">Włączona</string>
<string name="service_status_automatic">Automatycznie</string>
<string name="service_status_manual">Ręcznie</string>
<string name="foreground_service_notification_title">Aktywna w tle</string>
<string name="service_status_enabled_short">Wł.</string>
<string name="open_app">Otwórz</string>
<string name="foreground_service_notification_text"><xliff:g example="Powiadomienia o narażeniu">%1$s</xliff:g> jest uruchomiona w tle.</string>
<string name="foreground_service_notification_big_text">Wyłącz optymalizację baterii dla <xliff:g example="Usług microG">%1$s</xliff:g> lub zmień ustawienia powiadomień, aby ukryć to powiadomienie.</string>
<string name="list_item_see_all">Wyświetl wszystkie</string>
<string name="service_status_disabled_short">Wył.</string>
<string name="menu_game_managed">Zarządzane kontami gier</string>
</resources>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
--><resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="menu_advanced">Avançado</string>
<string name="list_no_item_none">Nenhum</string>
<string name="foreground_service_notification_text"><xliff:g example="Exposure Notification">%1$s</xliff:g> está em execução em segundo plano.</string>
<string name="service_status_enabled_short">Ativado</string>
<string name="service_status_disabled_short">Desativado</string>
<string name="foreground_service_notification_title">Ativo em segundo plano</string>
<string name="foreground_service_notification_big_text">Remova <xliff:g example="microG Services">%1$s</xliff:g> das configurações de otimizações de bateria ou mude as configurações da notificação para esconder esta notificação.</string>
<string name="list_item_see_all">Ver tudo</string>
<string name="open_app">Abrir</string>
<string name="service_status_disabled">Desativado</string>
<string name="service_status_enabled">Ativado</string>
<string name="service_status_automatic">Automático</string>
<string name="service_status_manual">Manual</string>
<string name="menu_game_managed">Contas de jogo gerenciadas</string>
</resources>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="foreground_service_notification_title">Ativo em segundo plano</string>
<string name="foreground_service_notification_text"><xliff:g example="Exposure Notification">%1$s</xliff:g> está em execução em segundo plano.</string>
<string name="foreground_service_notification_big_text">Exclua <xliff:g example="microG Services">%1$s</xliff:g> das configurações de otimizações de pilha ou mude as configurações da notificação para esconder esta notificação.</string>
<string name="menu_advanced">Avançado</string>
<string name="menu_game_managed">Contas de jogo gerenciadas</string>
<string name="list_no_item_none">Nenhum</string>
<string name="list_item_see_all">Ver tudo</string>
<string name="open_app">Abrir</string>
<string name="service_status_disabled">Desativado</string>
<string name="service_status_enabled">Ativado</string>
<string name="service_status_automatic">Automático</string>
<string name="service_status_manual">Manual</string>
<string name="service_status_enabled_short">Ligado</string>
<string name="service_status_disabled_short">Desligado</string>
</resources>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="foreground_service_notification_title">Activ în fundal</string>
<string name="menu_advanced">Avansat</string>
<string name="service_status_disabled">Dezactivat</string>
<string name="service_status_enabled_short">Pornit</string>
<string name="service_status_automatic">Automat</string>
<string name="service_status_enabled">Activat</string>
<string name="open_app">Deschide</string>
<string name="foreground_service_notification_text"><xliff:g example="Exposure Notification">%1$s</xliff:g> rulează în fundal.</string>
<string name="foreground_service_notification_big_text">Exclude <xliff:g example="microG Services">%1$s</xliff:g> din optimizările bateriei sau modifică setările de notificare pentru a ascunde această notificare.</string>
<string name="list_item_see_all">Arată tot</string>
<string name="service_status_disabled_short">Oprit</string>
<string name="list_no_item_none">Nimic</string>
<string name="service_status_manual">Manual</string>
<string name="menu_game_managed">Conturile de joc gestionate</string>
</resources>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
--><resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="foreground_service_notification_title">Фоновая активность</string>
<string name="foreground_service_notification_text"><xliff:g example="Exposure Notification">%1$s</xliff:g> работает в фоновом режиме.</string>
<string name="foreground_service_notification_big_text">Отключите экономию заряда батареи для <xliff:g example="microG Services">%1$s</xliff:g>, чтобы убрать это уведомление.</string>
<string name="menu_advanced">Дополнительно</string>
<string name="list_no_item_none">Пусто</string>
<string name="list_item_see_all">Показать всё</string>
<string name="open_app">Открыть</string>
<string name="service_status_disabled">Выключено</string>
<string name="service_status_enabled">Включено</string>
<string name="service_status_automatic">Автоматически</string>
<string name="service_status_manual">Вручную</string>
<string name="service_status_enabled_short">Вкл.</string>
<string name="service_status_disabled_short">Выкл.</string>
<string name="menu_game_managed">Управление игровыми аккаунтами</string>
</resources>

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
--><resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="menu_advanced">Напредно</string>
<string name="list_no_item_none">Ниједно</string>
<string name="foreground_service_notification_title">Активно у позадини</string>
<string name="service_status_disabled">Онемогућено</string>
<string name="service_status_enabled_short">Укључено</string>
<string name="service_status_automatic">Аутоматски</string>
<string name="service_status_enabled">Омогућено</string>
<string name="open_app">Отвори</string>
<string name="foreground_service_notification_text"><xliff:g example="Exposure Notification">%1$s</xliff:g> ради у позадини.</string>
<string name="foreground_service_notification_big_text">Искључите <xliff:g example="microG Services"> %1$s</xliff:g> из оптимизације батерије или промените подешавања обавештења да бисте сакрили ово обавештење.</string>
<string name="list_item_see_all">Види све</string>
<string name="service_status_disabled_short">Искључено</string>
<string name="service_status_manual">Ручно</string>
<string name="menu_game_managed">Управљање налозима игара</string>
</resources>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="foreground_service_notification_title">Aktiv i bakgrunden</string>
<string name="menu_advanced">Avancerat</string>
<string name="service_status_disabled">Inaktiverad</string>
<string name="service_status_enabled_short"></string>
<string name="service_status_automatic">Automatiskt</string>
<string name="service_status_enabled">Aktiverad</string>
<string name="open_app">Öppna</string>
<string name="foreground_service_notification_text"><xliff:g example="Exposure Notification">%1$s</xliff:g> körs i bakgrunden.</string>
<string name="foreground_service_notification_big_text">Exkludera <xliff:g example="microG Services">%1$s</xliff:g> från batterioptimering eller ändra aviseringsinställningar för att dölja detta meddelande.</string>
<string name="list_item_see_all">Se alla</string>
<string name="service_status_disabled_short">Av</string>
<string name="list_no_item_none">Ingen</string>
<string name="service_status_manual">Manuell</string>
<string name="menu_game_managed">Spelkonton hanterade</string>
</resources>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="foreground_service_notification_title">பின்னணியில் செயலில் உள்ளது</string>
<string name="menu_advanced">மேம்பட்ட</string>
<string name="list_no_item_none">எதுவுமில்லை</string>
<string name="list_item_see_all">அனைத்தையும் காண்க</string>
<string name="open_app">திற</string>
<string name="service_status_disabled">முடக்கப்பட்டது</string>
<string name="service_status_enabled">இயக்கப்பட்டது</string>
<string name="service_status_automatic">தானியங்கி</string>
<string name="service_status_manual">கையேடு</string>
<string name="service_status_enabled_short">ஆம்</string>
<string name="service_status_disabled_short">அணை</string>
<string name="foreground_service_notification_text"><xliff:g example="Exposure Notification">%1$s</xliff:g> பின்னணியில் இயங்குகிறது.</string>
<string name="foreground_service_notification_big_text">பேட்டரி மேம்படுத்தல்களிலிருந்து <xliff:g example="microG Services">%1$s</xliff:g>ஐ விலக்கு அல்லது இந்த அறிவிப்பை மறைக்க அறிவிப்பு அமைப்புகளை மாற்றவும்.</string>
<string name="menu_game_managed">நிர்வகிக்கப்படும் விளையாட்டு கணக்குகள்</string>
</resources>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="foreground_service_notification_title">ทำงานในพื้นหลัง</string>
<string name="foreground_service_notification_text"><xliff:g example="ระบบแจ้งเตือนเมื่อใกล้ชิดผู้ติดเชื้อ">%1$s</xliff:g> กำลังทำงานอยู่ในพื้นหลัง</string>
<string name="foreground_service_notification_big_text">ไม่รวม <xliff:g example="บริการ microG">%1$s</xliff:g> จากการเพิ่มประสิทธิภาพแบตเตอรี่หรือเปลี่ยนการตั้งค่าการแจ้งเตือนเพื่อซ่อนการแจ้งเตือนนี้</string>
<string name="menu_advanced">ขั้นสูง</string>
<string name="list_no_item_none">ไม่มี</string>
<string name="list_item_see_all">ดูทั้งหมด</string>
<string name="open_app">เปิด</string>
<string name="service_status_disabled">ปิดการทำงาน</string>
<string name="service_status_enabled">เปิดใช้งานแล้ว</string>
<string name="service_status_automatic">อัตโนมัติ</string>
<string name="service_status_manual">คู่มือ</string>
<string name="service_status_enabled_short">เปิด</string>
<string name="service_status_disabled_short">ปิด</string>
<string name="menu_game_managed">จัดการบัญชีเกม</string>
</resources>

Some files were not shown because too many files have changed in this diff Show more