diff --git a/library/src/com/android/annotations/VisibleForTesting.java b/library/src/com/android/annotations/VisibleForTesting.java new file mode 100644 index 0000000..d3afc6a --- /dev/null +++ b/library/src/com/android/annotations/VisibleForTesting.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2011 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.android.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Denotes that the class, method or field has its visibility relaxed so + * that unit tests can access it. + *

+ * The visibility argument can be used to specific what the original + * visibility should have been if it had not been made public or package-private for testing. + * The default is to consider the element private. + */ +@Retention(RetentionPolicy.SOURCE) +public @interface VisibleForTesting { + /** + * Intended visibility if the element had not been made public or package-private for + * testing. + */ + enum Visibility { + /** The element should be considered protected. */ + PROTECTED, + /** The element should be considered package-private. */ + PACKAGE, + /** The element should be considered private. */ + PRIVATE + } + + /** + * Intended visibility if the element had not been made public or package-private for testing. + * If not specified, one should assume the element originally intended to be private. + */ + Visibility visibility() default Visibility.PRIVATE; +} \ No newline at end of file diff --git a/library/src/com/android/setupwizardlib/util/Partner.java b/library/src/com/android/setupwizardlib/util/Partner.java new file mode 100644 index 0000000..5a81d4c --- /dev/null +++ b/library/src/com/android/setupwizardlib/util/Partner.java @@ -0,0 +1,157 @@ +/* + * 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.android.setupwizardlib.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.util.Log; + +import com.android.annotations.VisibleForTesting; + +/** + * Utilities to discover and interact with partner customizations. There can only be one set of + * customizations on a device, and it must be bundled with the system. + * + * Derived from com.android.launcher3/Partner.java + */ +public class Partner { + private static final String TAG = "(SUW) Partner"; + + /** Marker action used to discover partner */ + private static final String + ACTION_PARTNER_CUSTOMIZATION = "com.android.setupwizard.action.PARTNER_CUSTOMIZATION"; + + private static boolean sSearched = false; + private static Partner sPartner; + + /** + * Convenience to get 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, int id) { + final ResourceEntry entry = getResourceEntry(context, id); + return entry.resources.getDrawable(entry.id); + } + + /** + * Convenience to get 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, int id) { + final ResourceEntry entry = getResourceEntry(context, id); + return entry.resources.getString(entry.id); + } + + /** + * Find 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, int id) { + final Partner partner = Partner.get(context); + if (partner == null) { + return new ResourceEntry(context.getResources(), id); + } else { + final Resources ourResources = context.getResources(); + final String name = ourResources.getResourceEntryName(id); + final String type = ourResources.getResourceTypeName(id); + final int partnerId = partner.getIdentifier(name, type); + return new ResourceEntry(partner.mResources, partnerId); + } + } + + public static class ResourceEntry { + public Resources resources; + public int id; + + ResourceEntry(Resources resources, int id) { + this.resources = resources; + this.id = id; + } + } + + /** + * Find and return partner details, or {@code null} if none exists. A partner package is marked + * by a broadcast receiver declared in the manifest that handles the + * 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 (!sSearched) { + PackageManager pm = context.getPackageManager(); + final Intent intent = new Intent(ACTION_PARTNER_CUSTOMIZATION); + for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) { + 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); + sPartner = new Partner(appInfo.packageName, res); + break; + } catch (NameNotFoundException e) { + Log.w(TAG, "Failed to find resources for " + appInfo.packageName); + } + } + } + sSearched = true; + } + return sPartner; + } + + @VisibleForTesting + public static synchronized void resetForTesting() { + sSearched = false; + sPartner = null; + } + + private final String mPackageName; + private final Resources mResources; + + private Partner(String packageName, Resources res) { + mPackageName = packageName; + mResources = res; + } + + public String getPackageName() { + return mPackageName; + } + + public Resources getResources() { + return mResources; + } + + public int getIdentifier(String name, String defType) { + return mResources.getIdentifier(name, defType, mPackageName); + } +} diff --git a/library/test/res/values/config.xml b/library/test/res/values/config.xml new file mode 100644 index 0000000..bda4f54 --- /dev/null +++ b/library/test/res/values/config.xml @@ -0,0 +1,23 @@ + + + + + + + 5000 + + diff --git a/library/test/src/com/android/setupwizardlib/test/PartnerTest.java b/library/test/src/com/android/setupwizardlib/test/PartnerTest.java new file mode 100644 index 0000000..c5d698f --- /dev/null +++ b/library/test/src/com/android/setupwizardlib/test/PartnerTest.java @@ -0,0 +1,157 @@ +/* + * 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.android.setupwizardlib.test; + +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.test.InstrumentationTestCase; +import android.test.mock.MockPackageManager; +import android.test.suitebuilder.annotation.SmallTest; + +import com.android.setupwizardlib.util.Partner; +import com.android.setupwizardlib.util.Partner.ResourceEntry; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class PartnerTest extends InstrumentationTestCase { + + private TestContext mTestContext; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mTestContext = new TestContext(getInstrumentation().getTargetContext()); + Partner.resetForTesting(); + } + + @SmallTest + public void testLoadPartner() { + mTestContext.partnerList = Arrays.asList( + createResolveInfo("hocus.pocus", false), + createResolveInfo("com.android.setupwizardlib.test", true) + ); + + Partner partner = Partner.get(mTestContext); + assertNotNull("Partner should not be null", partner); + } + + @SmallTest + public void testLoadNoPartner() { + mTestContext.partnerList = new ArrayList<>(); + + Partner partner = Partner.get(mTestContext); + assertNull("Partner should be null", partner); + } + + @SmallTest + public void testLoadNonSystemPartner() { + mTestContext.partnerList = Arrays.asList( + createResolveInfo("hocus.pocus", false), + createResolveInfo("com.android.setupwizardlib.test", false) + ); + + Partner partner = Partner.get(mTestContext); + assertNull("Partner should be null", partner); + } + + public void testLoadPartnerValue() { + mTestContext.partnerList = Arrays.asList( + createResolveInfo("hocus.pocus", false), + createResolveInfo("com.android.setupwizardlib.test", true) + ); + + ResourceEntry entry = + Partner.getResourceEntry(mTestContext, R.integer.suwTransitionDuration); + int partnerValue = entry.resources.getInteger(entry.id); + assertEquals("Partner value should be overlaid to 5000", 5000, partnerValue); + } + + public void testLoadDefaultValue() { + mTestContext.partnerList = Arrays.asList( + createResolveInfo("hocus.pocus", false), + createResolveInfo("com.android.setupwizardlib.test", true) + ); + + ResourceEntry entry = + Partner.getResourceEntry(mTestContext, R.color.suw_navbar_text_dark); + int partnerValue = entry.resources.getColor(entry.id); + assertEquals("Partner value should default to 0xdeffffff", 0xdeffffff, partnerValue); + } + + private ResolveInfo createResolveInfo(String packageName, boolean isSystem) { + ResolveInfo info = new ResolveInfo(); + info.resolvePackageName = packageName; + ActivityInfo activityInfo = new ActivityInfo(); + ApplicationInfo appInfo = new ApplicationInfo(); + appInfo.flags = isSystem ? ApplicationInfo.FLAG_SYSTEM : 0; + appInfo.packageName = packageName; + activityInfo.applicationInfo = appInfo; + activityInfo.packageName = packageName; + activityInfo.name = packageName; + info.activityInfo = activityInfo; + return info; + } + + private static class TestPackageManager extends MockPackageManager { + + private Context mTestContext; + + public TestPackageManager(Context testContext) { + mTestContext = testContext; + } + + @Override + public Resources getResourcesForApplication(ApplicationInfo app) { + if (app != null && "com.android.setupwizardlib.test".equals(app.packageName)) { + return mTestContext.getResources(); + } else { + return super.getResourcesForApplication(app); + } + } + + @Override + public List queryBroadcastReceivers(Intent intent, int flags) { + if ("com.android.setupwizard.action.PARTNER_CUSTOMIZATION".equals(intent.getAction())) { + return ((TestContext) mTestContext).partnerList; + } else { + return super.queryBroadcastReceivers(intent, flags); + } + } + } + + private static class TestContext extends ContextWrapper { + + public List partnerList; + + public TestContext(Context context) { + super(context); + } + + @Override + public PackageManager getPackageManager() { + return new TestPackageManager(this); + } + } +}