| /* |
| * Copyright (C) 2015 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 com.google.android.setupdesign.util; |
| |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageManager.NameNotFoundException; |
| import android.content.pm.ResolveInfo; |
| import android.content.res.Resources; |
| import android.graphics.drawable.Drawable; |
| import android.os.Build.VERSION; |
| import android.os.Build.VERSION_CODES; |
| import androidx.annotation.AnyRes; |
| import androidx.annotation.ColorRes; |
| import androidx.annotation.DrawableRes; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.StringRes; |
| import androidx.annotation.VisibleForTesting; |
| import android.util.Log; |
| import java.util.List; |
| |
| /** |
| * Utilities to discover and interact with partner customizations. An overlay package is one that |
| * registers the broadcast receiver for {@code com.android.setupwizard.action.PARTNER_CUSTOMIZATION} |
| * in its manifest. There can only be one customization APK on a device, and it must be bundled with |
| * the system. |
| * |
| * <p>Derived from {@code com.android.launcher3/Partner.java} |
| */ |
| public class Partner { |
| |
| private static final String TAG = "(setupdesign) Partner"; |
| |
| /** Marker action used to discover partner. */ |
| private static final String ACTION_PARTNER_CUSTOMIZATION = |
| "com.android.setupwizard.action.PARTNER_CUSTOMIZATION"; |
| |
| private static boolean searched = false; |
| @Nullable private static Partner partner; |
| |
| /** |
| * Gets a drawable from partner overlay, or if not available, the drawable from the original |
| * context. |
| * |
| * @see #getResourceEntry(android.content.Context, int) |
| */ |
| public static Drawable getDrawable(Context context, @DrawableRes int id) { |
| final ResourceEntry entry = getResourceEntry(context, id); |
| return entry.resources.getDrawable(entry.id); |
| } |
| |
| /** |
| * Gets a string from partner overlay, or if not available, the string from the original context. |
| * |
| * @see #getResourceEntry(android.content.Context, int) |
| */ |
| public static String getString(Context context, @StringRes int id) { |
| final ResourceEntry entry = getResourceEntry(context, id); |
| return entry.resources.getString(entry.id); |
| } |
| |
| /** |
| * Gets a color from partner overlay, or if not available, the color from the original context. |
| */ |
| public static int getColor(Context context, @ColorRes int id) { |
| final ResourceEntry resourceEntry = getResourceEntry(context, id); |
| return resourceEntry.resources.getColor(resourceEntry.id); |
| } |
| |
| /** |
| * Gets a CharSequence from partner overlay, or if not available, the text from the original |
| * context. |
| */ |
| public static CharSequence getText(Context context, @StringRes int id) { |
| final ResourceEntry entry = getResourceEntry(context, id); |
| return entry.resources.getText(entry.id); |
| } |
| |
| /** |
| * Finds an entry of resource in the overlay package provided by partners. It will first look for |
| * the resource in the overlay package, and if not available, will return the one in the original |
| * context. |
| * |
| * @return a ResourceEntry in the partner overlay's resources, if one is defined. Otherwise the |
| * resources from the original context is returned. Clients can then get the resource by |
| * {@code entry.resources.getString(entry.id)}, or other methods available in {@link |
| * android.content.res.Resources}. |
| */ |
| public static ResourceEntry getResourceEntry(Context context, @AnyRes int id) { |
| final Partner partner = Partner.get(context); |
| if (partner != null) { |
| final Resources ourResources = context.getResources(); |
| final String name = ourResources.getResourceEntryName(id); |
| final String type = ourResources.getResourceTypeName(id); |
| final int partnerId = partner.getIdentifier(name, type); |
| if (partnerId != 0) { |
| return new ResourceEntry(partner.getPackageName(), partner.resources, partnerId, true); |
| } |
| } |
| return new ResourceEntry(context.getPackageName(), context.getResources(), id, false); |
| } |
| |
| public static class ResourceEntry { |
| public String packageName; |
| public Resources resources; |
| public int id; |
| public boolean isOverlay; |
| |
| ResourceEntry(String packageName, Resources resources, int id, boolean isOverlay) { |
| this.packageName = packageName; |
| this.resources = resources; |
| this.id = id; |
| this.isOverlay = isOverlay; |
| } |
| } |
| |
| /** |
| * Finds and returns partner details, or {@code null} if none exists. A partner package is marked |
| * by a broadcast receiver declared in the manifest that handles the {@code |
| * com.android.setupwizard.action.PARTNER_CUSTOMIZATION} intent action. The overlay package must |
| * also be a system package. |
| */ |
| public static synchronized Partner get(Context context) { |
| if (!searched) { |
| PackageManager pm = context.getPackageManager(); |
| final Intent intent = new Intent(ACTION_PARTNER_CUSTOMIZATION); |
| List<ResolveInfo> receivers; |
| if (VERSION.SDK_INT >= VERSION_CODES.N) { |
| receivers = |
| pm.queryBroadcastReceivers( |
| intent, |
| PackageManager.MATCH_SYSTEM_ONLY |
| | PackageManager.MATCH_DIRECT_BOOT_AWARE |
| | PackageManager.MATCH_DIRECT_BOOT_UNAWARE |
| | PackageManager.MATCH_DISABLED_COMPONENTS); |
| } else { |
| // On versions before N, direct boot doesn't exist. And the MATCH_SYSTEM_ONLY flag |
| // doesn't exist so we filter for system apps in code below. |
| receivers = pm.queryBroadcastReceivers(intent, PackageManager.GET_DISABLED_COMPONENTS); |
| } |
| |
| for (ResolveInfo info : receivers) { |
| if (info.activityInfo == null) { |
| continue; |
| } |
| final ApplicationInfo appInfo = info.activityInfo.applicationInfo; |
| if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { |
| try { |
| final Resources res = pm.getResourcesForApplication(appInfo); |
| partner = new Partner(appInfo.packageName, res); |
| break; |
| } catch (NameNotFoundException e) { |
| Log.w(TAG, "Failed to find resources for " + appInfo.packageName); |
| } |
| } |
| } |
| searched = true; |
| } |
| return partner; |
| } |
| |
| @VisibleForTesting |
| public static synchronized void resetForTesting() { |
| searched = false; |
| partner = null; |
| } |
| |
| private final String packageName; |
| private final Resources resources; |
| |
| private Partner(String packageName, Resources res) { |
| this.packageName = packageName; |
| resources = res; |
| } |
| |
| public String getPackageName() { |
| return packageName; |
| } |
| |
| public Resources getResources() { |
| return resources; |
| } |
| |
| public int getIdentifier(String name, String defType) { |
| return resources.getIdentifier(name, defType, packageName); |
| } |
| } |