mirror of https://github.com/M66B/FairEmail.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
337 lines
13 KiB
337 lines
13 KiB
/*
|
|
* Copyright 2021 The Android Open Source Project
|
|
*
|
|
* 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 androidx.emoji2.text;
|
|
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.pm.ApplicationInfo;
|
|
import android.content.pm.PackageInfo;
|
|
import android.content.pm.PackageManager;
|
|
import android.content.pm.ProviderInfo;
|
|
import android.content.pm.ResolveInfo;
|
|
import android.content.pm.Signature;
|
|
import android.os.Build;
|
|
import android.util.Log;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.RequiresApi;
|
|
import androidx.annotation.RestrictTo;
|
|
import androidx.annotation.VisibleForTesting;
|
|
import androidx.core.provider.FontRequest;
|
|
import androidx.core.util.Preconditions;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* The default config will use downloadable fonts to fetch the emoji compat font file.
|
|
*
|
|
* <p>It will automatically fetch the emoji compat font from a {@code ContentProvider} that is
|
|
* installed on the devices system image, if present.</p>
|
|
*
|
|
* <p>You should use this if you want the default emoji font from a system installed
|
|
* downloadable fonts provider. This is the recommended behavior for all applications unless
|
|
* they install a custom emoji font.</p>
|
|
*
|
|
* <p>You may need to specialize the configuration beyond this default config in some
|
|
* situations:</p>
|
|
* <ul>
|
|
* <li>If you are trying to install a custom emoji font through downloadable fonts use
|
|
* {@link FontRequestEmojiCompatConfig} instead of this method.</li>
|
|
* <li>If you're trying to bundle an emoji font with your APK use {@code
|
|
* BundledEmojiCompatConfig} in the {@code emoji2-bundled} artifact.</li>
|
|
* <li>If you are building an APK that will be installed on devices that won't have a
|
|
* downloadable fonts provider, use {@code BundledEmojiCompatConfig}.</li>
|
|
* </ul>
|
|
*
|
|
* <p>The downloadable font provider used by {@code DefaultEmojiCompatConfig} always satisfies
|
|
* the following contract:</p>
|
|
* <ol>
|
|
* <li>It <i>MUST</i> provide an intent filter for {@code androidx.content.action.LOAD_EMOJI_FONT}.
|
|
* </li>
|
|
* <li>It <i>MUST</i> respond to the query {@code emojicompat-emoji-font} with a valid emoji compat
|
|
* font file including metadata.</li>
|
|
* <li>It <i>MUST</i> provide fonts via the same contract as downloadable fonts.</li>
|
|
* <li>It <i>MUST</i> be installed in the system image.</li>
|
|
* </ol>
|
|
*/
|
|
public final class DefaultEmojiCompatConfig {
|
|
/**
|
|
* This class cannot be instantiated.
|
|
*
|
|
* @see DefaultEmojiCompatConfig#create
|
|
*/
|
|
private DefaultEmojiCompatConfig() {
|
|
}
|
|
|
|
/**
|
|
* Get the default emoji compat config for this device.
|
|
*
|
|
* You may further configure the returned config before passing it to {@link EmojiCompat#init}.
|
|
*
|
|
* Each call to this method will return a new EmojiCompat.Config, so changes to the returned
|
|
* object will not modify future return values.
|
|
*
|
|
* @param context context for lookup
|
|
* @return A valid config for downloading the emoji compat font, or null if no font provider
|
|
* could be found.
|
|
*/
|
|
@Nullable
|
|
public static FontRequestEmojiCompatConfig create(@NonNull Context context) {
|
|
return (FontRequestEmojiCompatConfig) new DefaultEmojiCompatConfigFactory(null)
|
|
.create(context);
|
|
}
|
|
|
|
/**
|
|
* Actual factory for generating default emoji configs, does service locator lookup internally.
|
|
*
|
|
* @see DefaultEmojiCompatConfig#create
|
|
*/
|
|
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
|
public static class DefaultEmojiCompatConfigFactory {
|
|
private static final @NonNull String TAG = "emoji2.text.DefaultEmojiConfig";
|
|
private static final @NonNull String INTENT_LOAD_EMOJI_FONT =
|
|
"androidx.content.action.LOAD_EMOJI_FONT";
|
|
private static final @NonNull String DEFAULT_EMOJI_QUERY = "emojicompat-emoji-font";
|
|
private final DefaultEmojiCompatConfigHelper mHelper;
|
|
|
|
/**
|
|
*/
|
|
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
|
public DefaultEmojiCompatConfigFactory(@Nullable DefaultEmojiCompatConfigHelper helper) {
|
|
mHelper = helper != null ? helper : getHelperForApi();
|
|
}
|
|
|
|
/**
|
|
* @see DefaultEmojiCompatConfig#create
|
|
*/
|
|
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
|
@Nullable
|
|
public EmojiCompat.Config create(@NonNull Context context) {
|
|
return configOrNull(context, (FontRequest) queryForDefaultFontRequest(context));
|
|
}
|
|
|
|
/**
|
|
* Create a new Config if fontRequest is not null
|
|
* @param context context for the config
|
|
* @param fontRequest optional font request
|
|
* @return a new config if fontRequest is not null
|
|
*/
|
|
@Nullable
|
|
private EmojiCompat.Config configOrNull(@NonNull Context context,
|
|
@Nullable FontRequest fontRequest) {
|
|
if (fontRequest == null) {
|
|
return null;
|
|
} else {
|
|
return new FontRequestEmojiCompatConfig(context, fontRequest);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find the installed font provider and return a FontInfo that describes it.
|
|
* @param context context for getting package manager
|
|
* @return valid FontRequest, or null if no provider could be found
|
|
*/
|
|
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
|
@Nullable
|
|
@VisibleForTesting
|
|
FontRequest queryForDefaultFontRequest(@NonNull Context context) {
|
|
PackageManager packageManager = context.getPackageManager();
|
|
// throw here since the developer has provided an atypical Context
|
|
Preconditions.checkNotNull(packageManager,
|
|
"Package manager required to locate emoji font provider");
|
|
ProviderInfo providerInfo = queryDefaultInstalledContentProvider(packageManager);
|
|
if (providerInfo == null) return null;
|
|
|
|
try {
|
|
return generateFontRequestFrom(providerInfo, packageManager);
|
|
} catch (PackageManager.NameNotFoundException e) {
|
|
Log.wtf(TAG, e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Look up a ContentProvider that provides emoji fonts that's installed with the system.
|
|
*
|
|
* @param packageManager package manager from a Context
|
|
* @return a ResolveInfo for a system installed content provider, or null if none found
|
|
*/
|
|
@Nullable
|
|
private ProviderInfo queryDefaultInstalledContentProvider(
|
|
@NonNull PackageManager packageManager) {
|
|
List<ResolveInfo> providers = mHelper.queryIntentContentProviders(packageManager,
|
|
new Intent(INTENT_LOAD_EMOJI_FONT), 0);
|
|
|
|
for (ResolveInfo resolveInfo : providers) {
|
|
ProviderInfo providerInfo = mHelper.getProviderInfo(resolveInfo);
|
|
if (hasFlagSystem(providerInfo)) {
|
|
return providerInfo;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param providerInfo optional ProviderInfo that describes a content provider
|
|
* @return true if this provider info is from an application with
|
|
* {@link ApplicationInfo#FLAG_SYSTEM}
|
|
*/
|
|
private boolean hasFlagSystem(@Nullable ProviderInfo providerInfo) {
|
|
return providerInfo != null
|
|
&& providerInfo.applicationInfo != null
|
|
&& (providerInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM)
|
|
== ApplicationInfo.FLAG_SYSTEM;
|
|
}
|
|
|
|
/**
|
|
* Generate a full FontRequest from a ResolveInfo that describes a ContentProvider.
|
|
*
|
|
* @param providerInfo description of content provider to generate a FontRequest from
|
|
* @return a valid font request
|
|
* @throws NullPointerException if the passed resolveInfo has a null providerInfo.
|
|
*/
|
|
@NonNull
|
|
private FontRequest generateFontRequestFrom(
|
|
@NonNull ProviderInfo providerInfo,
|
|
@NonNull PackageManager packageManager
|
|
) throws PackageManager.NameNotFoundException {
|
|
String providerAuthority = providerInfo.authority;
|
|
String providerPackage = providerInfo.packageName;
|
|
|
|
Signature[] signingSignatures = mHelper.getSigningSignatures(packageManager,
|
|
providerPackage);
|
|
List<List<byte[]>> signatures = convertToByteArray(signingSignatures);
|
|
return new FontRequest(providerAuthority, providerPackage, DEFAULT_EMOJI_QUERY,
|
|
signatures);
|
|
}
|
|
|
|
/**
|
|
* Convert signatures into a form usable by a FontConfig
|
|
*/
|
|
@NonNull
|
|
private List<List<byte[]>> convertToByteArray(@NonNull Signature[] signatures) {
|
|
List<byte[]> shaList = new ArrayList<>();
|
|
for (Signature signature : signatures) {
|
|
shaList.add(signature.toByteArray());
|
|
}
|
|
return Collections.singletonList(shaList);
|
|
}
|
|
|
|
/**
|
|
* @return the right DefaultEmojiCompatConfigHelper for the device API
|
|
*/
|
|
@NonNull
|
|
private static DefaultEmojiCompatConfigHelper getHelperForApi() {
|
|
if (Build.VERSION.SDK_INT >= 28) {
|
|
return new DefaultEmojiCompatConfigHelper_API28();
|
|
} else if (Build.VERSION.SDK_INT >= 19) {
|
|
return new DefaultEmojiCompatConfigHelper_API19();
|
|
} else {
|
|
return new DefaultEmojiCompatConfigHelper();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to lookup signatures in package manager.
|
|
*
|
|
*/
|
|
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
|
public static class DefaultEmojiCompatConfigHelper {
|
|
/**
|
|
* Get the signing signatures for a package in package manager.
|
|
*/
|
|
@SuppressWarnings("deprecation") // replaced in API 28
|
|
@NonNull
|
|
public Signature[] getSigningSignatures(@NonNull PackageManager packageManager,
|
|
@NonNull String providerPackage) throws PackageManager.NameNotFoundException {
|
|
PackageInfo packageInfoForSignatures = packageManager.getPackageInfo(providerPackage,
|
|
PackageManager.GET_SIGNATURES);
|
|
return packageInfoForSignatures.signatures;
|
|
}
|
|
|
|
/**
|
|
* Get the content provider by intent.
|
|
*/
|
|
@NonNull
|
|
public List<ResolveInfo> queryIntentContentProviders(@NonNull PackageManager packageManager,
|
|
@NonNull Intent intent, int flags) {
|
|
return Collections.emptyList();
|
|
}
|
|
|
|
/**
|
|
* Get a ProviderInfo, if present, from a ResolveInfo
|
|
* @param resolveInfo the subject
|
|
* @return resolveInfo.providerInfo above API 19
|
|
*/
|
|
@Nullable
|
|
public ProviderInfo getProviderInfo(@NonNull ResolveInfo resolveInfo) {
|
|
throw new IllegalStateException("Unable to get provider info prior to API 19");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Actually do lookups > API 19
|
|
*
|
|
*/
|
|
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
|
@RequiresApi(19)
|
|
public static class DefaultEmojiCompatConfigHelper_API19
|
|
extends DefaultEmojiCompatConfigHelper {
|
|
@NonNull
|
|
@Override
|
|
@SuppressWarnings("deprecation")
|
|
public List<ResolveInfo> queryIntentContentProviders(@NonNull PackageManager packageManager,
|
|
@NonNull Intent intent, int flags) {
|
|
return packageManager.queryIntentContentProviders(intent, flags);
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public ProviderInfo getProviderInfo(@NonNull ResolveInfo resolveInfo) {
|
|
return resolveInfo.providerInfo;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to lookup signatures in package manager > API 28
|
|
*/
|
|
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
|
@RequiresApi(28)
|
|
public static class DefaultEmojiCompatConfigHelper_API28
|
|
extends DefaultEmojiCompatConfigHelper_API19 {
|
|
@SuppressWarnings("deprecation") // using deprecated API to match exact behavior in core
|
|
@Override
|
|
@NonNull
|
|
public Signature[] getSigningSignatures(@NonNull PackageManager packageManager,
|
|
@NonNull String providerPackage)
|
|
throws PackageManager.NameNotFoundException {
|
|
// This uses the deprecated GET_SIGNATURES currently to match the behavior in Core.
|
|
// When that behavior changes, we will need to update this method.
|
|
|
|
// Alternatively, you may at that time introduce a new config option that allows
|
|
// skipping signature validations to avoid this code sync.
|
|
PackageInfo packageInfoForSignatures = packageManager.getPackageInfo(providerPackage,
|
|
PackageManager.GET_SIGNATURES);
|
|
return packageInfoForSignatures.signatures;
|
|
}
|
|
}
|
|
}
|