Repo Created
This commit is contained in:
parent
eb305e2886
commit
a8c22c65db
4784 changed files with 329907 additions and 2 deletions
63
play-services-base/core/build.gradle
Normal file
63
play-services-base/core/build.gradle
Normal 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'
|
||||
45
play-services-base/core/package/build.gradle
Normal file
45
play-services-base/core/package/build.gradle
Normal 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
|
||||
}
|
||||
}
|
||||
23
play-services-base/core/package/src/main/AndroidManifest.xml
Normal file
23
play-services-base/core/package/src/main/AndroidManifest.xml
Normal 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>
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
}
|
||||
45
play-services-base/core/src/main/AndroidManifest.xml
Normal file
45
play-services-base/core/src/main/AndroidManifest.xml
Normal 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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 "";
|
||||
}
|
||||
|
|
@ -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 {
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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())) }
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)}"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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"}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
17
play-services-base/core/src/main/res/drawable/ic_open.xml
Normal file
17
play-services-base/core/src/main/res/drawable/ic_open.xml
Normal 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>
|
||||
16
play-services-base/core/src/main/res/drawable/ic_radio.xml
Normal file
16
play-services-base/core/src/main/res/drawable/ic_radio.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
50
play-services-base/core/src/main/res/layout/list_no_item.xml
Normal file
50
play-services-base/core/src/main/res/layout/list_no_item.xml
Normal 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>
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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" />
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
16
play-services-base/core/src/main/res/values-ar/strings.xml
Normal file
16
play-services-base/core/src/main/res/values-ar/strings.xml
Normal 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">استثني <xliff:g example=\"microG Services\">%1$s</xliff:g> من توفير شحن البطارية أو غير إعدادات الإشعارات لإخفاء هذا اﻹشعار.</string>
|
||||
<string name="foreground_service_notification_text"><xliff:g example=\"Exposure Notification\">%1$s</xliff:g> يعمل في الخلفية.</string>
|
||||
</resources>
|
||||
12
play-services-base/core/src/main/res/values-ast/strings.xml
Normal file
12
play-services-base/core/src/main/res/values-ast/strings.xml
Normal 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">Sí</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>
|
||||
15
play-services-base/core/src/main/res/values-az/strings.xml
Normal file
15
play-services-base/core/src/main/res/values-az/strings.xml
Normal 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">Aç</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>
|
||||
25
play-services-base/core/src/main/res/values-be/strings.xml
Normal file
25
play-services-base/core/src/main/res/values-be/strings.xml
Normal 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>
|
||||
17
play-services-base/core/src/main/res/values-cs/strings.xml
Normal file
17
play-services-base/core/src/main/res/values-cs/strings.xml
Normal 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>
|
||||
20
play-services-base/core/src/main/res/values-de/strings.xml
Normal file
20
play-services-base/core/src/main/res/values-de/strings.xml
Normal 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>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
19
play-services-base/core/src/main/res/values-es/strings.xml
Normal file
19
play-services-base/core/src/main/res/values-es/strings.xml
Normal 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>
|
||||
17
play-services-base/core/src/main/res/values-fa/strings.xml
Normal file
17
play-services-base/core/src/main/res/values-fa/strings.xml
Normal 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>
|
||||
13
play-services-base/core/src/main/res/values-fi/strings.xml
Normal file
13
play-services-base/core/src/main/res/values-fi/strings.xml
Normal 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>
|
||||
17
play-services-base/core/src/main/res/values-fil/strings.xml
Normal file
17
play-services-base/core/src/main/res/values-fil/strings.xml
Normal 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>
|
||||
20
play-services-base/core/src/main/res/values-fr/strings.xml
Normal file
20
play-services-base/core/src/main/res/values-fr/strings.xml
Normal 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>
|
||||
17
play-services-base/core/src/main/res/values-ga/strings.xml
Normal file
17
play-services-base/core/src/main/res/values-ga/strings.xml
Normal 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>
|
||||
17
play-services-base/core/src/main/res/values-in/strings.xml
Normal file
17
play-services-base/core/src/main/res/values-in/strings.xml
Normal 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>
|
||||
17
play-services-base/core/src/main/res/values-is/strings.xml
Normal file
17
play-services-base/core/src/main/res/values-is/strings.xml
Normal 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>
|
||||
17
play-services-base/core/src/main/res/values-it/strings.xml
Normal file
17
play-services-base/core/src/main/res/values-it/strings.xml
Normal 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>
|
||||
19
play-services-base/core/src/main/res/values-ja/strings.xml
Normal file
19
play-services-base/core/src/main/res/values-ja/strings.xml
Normal 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>
|
||||
17
play-services-base/core/src/main/res/values-ko/strings.xml
Normal file
17
play-services-base/core/src/main/res/values-ko/strings.xml
Normal 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>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
17
play-services-base/core/src/main/res/values-ml/strings.xml
Normal file
17
play-services-base/core/src/main/res/values-ml/strings.xml
Normal 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>
|
||||
|
|
@ -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">På</string>
|
||||
<string name="service_status_disabled_short">Av</string>
|
||||
</resources>
|
||||
16
play-services-base/core/src/main/res/values-nl/strings.xml
Normal file
16
play-services-base/core/src/main/res/values-nl/strings.xml
Normal 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 <xliff:g example=“microG Services”>%1$s</xliff:g> 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>
|
||||
20
play-services-base/core/src/main/res/values-pl/strings.xml
Normal file
20
play-services-base/core/src/main/res/values-pl/strings.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
17
play-services-base/core/src/main/res/values-pt/strings.xml
Normal file
17
play-services-base/core/src/main/res/values-pt/strings.xml
Normal 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>
|
||||
17
play-services-base/core/src/main/res/values-ro/strings.xml
Normal file
17
play-services-base/core/src/main/res/values-ro/strings.xml
Normal 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>
|
||||
20
play-services-base/core/src/main/res/values-ru/strings.xml
Normal file
20
play-services-base/core/src/main/res/values-ru/strings.xml
Normal 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>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
</resources>
|
||||
20
play-services-base/core/src/main/res/values-sr/strings.xml
Normal file
20
play-services-base/core/src/main/res/values-sr/strings.xml
Normal 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>
|
||||
17
play-services-base/core/src/main/res/values-sv/strings.xml
Normal file
17
play-services-base/core/src/main/res/values-sv/strings.xml
Normal 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">På</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>
|
||||
17
play-services-base/core/src/main/res/values-ta/strings.xml
Normal file
17
play-services-base/core/src/main/res/values-ta/strings.xml
Normal 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>
|
||||
17
play-services-base/core/src/main/res/values-th/strings.xml
Normal file
17
play-services-base/core/src/main/res/values-th/strings.xml
Normal 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>
|
||||
16
play-services-base/core/src/main/res/values-tr/strings.xml
Normal file
16
play-services-base/core/src/main/res/values-tr/strings.xml
Normal 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">Arka planda aktif</string>
|
||||
<string name="foreground_service_notification_text"><xliff:g example="Maruz Kalma Bildirimi">%1$s</xliff:g> arka planda çalışıyor.</string>
|
||||
<string name="menu_advanced">Gelişmiş</string>
|
||||
<string name="list_no_item_none">Hiçbiri</string>
|
||||
<string name="list_item_see_all">Hepsini gör</string>
|
||||
<string name="service_status_manual">Manuel</string>
|
||||
<string name="service_status_enabled_short">Açık</string>
|
||||
<string name="service_status_disabled_short">Kapalı</string>
|
||||
<string name="foreground_service_notification_big_text">Bildirimi gizlemek için <xliff:g example="microG Servisleri">%1$s</xliff:g>\'ni pil optimizasyonlarından hariç tutun veya bildirim ayarlarını değiştirin.</string>
|
||||
<string name="open_app">Aç</string>
|
||||
<string name="service_status_disabled">Devre dışı</string>
|
||||
<string name="service_status_enabled">Etkin</string>
|
||||
<string name="service_status_automatic">Otomatik</string>
|
||||
</resources>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue