1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package android.testing; 16 17 import static org.mockito.Mockito.mock; 18 import static org.mockito.Mockito.when; 19 import static org.mockito.Mockito.withSettings; 20 21 import android.content.Context; 22 import android.content.res.Configuration; 23 import android.content.res.Resources; 24 import android.util.Log; 25 import android.util.SparseArray; 26 27 import org.mockito.invocation.InvocationOnMock; 28 29 /** 30 * Provides a version of Resources that defaults to all existing resources, but can have ids 31 * changed to return specific values. 32 * <p> 33 * TestableResources are lazily initialized, be sure to call 34 * {@link TestableContext#ensureTestableResources} before your tested code has an opportunity 35 * to cache {@link Context#getResources}. 36 * </p> 37 */ 38 public class TestableResources { 39 40 private static final String TAG = "TestableResources"; 41 private final Resources mResources; 42 private final SparseArray<Object> mOverrides = new SparseArray<>(); 43 44 /** Creates a TestableResources instance that calls through to the given real Resources. */ TestableResources(Resources realResources)45 public TestableResources(Resources realResources) { 46 mResources = mock(Resources.class, withSettings() 47 .spiedInstance(realResources) 48 .defaultAnswer(this::answer)); 49 } 50 51 /** 52 * Gets the implementation of Resources that will return overridden values when called. 53 */ getResources()54 public Resources getResources() { 55 return mResources; 56 } 57 58 /** 59 * Sets a configuration for {@link #getResources()} to return to allow custom configs to 60 * be set and tested. 61 * 62 * @param configuration the configuration to return from resources. 63 */ overrideConfiguration(Configuration configuration)64 public void overrideConfiguration(Configuration configuration) { 65 when(mResources.getConfiguration()).thenReturn(configuration); 66 } 67 68 /** 69 * Sets the return value for the specified resource id. 70 * <p> 71 * Since resource ids are unique there is a single addOverride that will override the value 72 * whenever it is gotten regardless of which method is used (i.e. getColor or getDrawable). 73 * </p> 74 * 75 * @param id The resource id to be overridden 76 * @param value The value of the resource, null to cause a {@link Resources.NotFoundException} 77 * when gotten. 78 */ addOverride(int id, Object value)79 public void addOverride(int id, Object value) { 80 mOverrides.put(id, value); 81 } 82 83 /** 84 * Removes the override for the specified id. 85 * <p> 86 * This should be called over addOverride(id, null), because specifying a null value will 87 * cause a {@link Resources.NotFoundException} whereas removing the override will actually 88 * switch back to returning the default/real value of the resource. 89 * </p> 90 */ removeOverride(int id)91 public void removeOverride(int id) { 92 mOverrides.remove(id); 93 } 94 answer(InvocationOnMock invocationOnMock)95 private Object answer(InvocationOnMock invocationOnMock) throws Throwable { 96 // Only try to override methods with an integer first argument 97 if (invocationOnMock.getArguments().length > 0) { 98 Object argument = invocationOnMock.getArgument(0); 99 if (argument instanceof Integer) { 100 try { 101 int id = (Integer)argument; 102 int index = mOverrides.indexOfKey(id); 103 if (index >= 0) { 104 Object value = mOverrides.valueAt(index); 105 if (value == null) throw new Resources.NotFoundException(); 106 return value; 107 } 108 } catch (Resources.NotFoundException e) { 109 // Let through NotFoundException. 110 throw e; 111 } catch (Throwable t) { 112 // Generic catching for the many things that can go wrong, fall back to 113 // the real implementation. 114 Log.i(TAG, "Falling back to default resources call " + t); 115 } 116 } 117 } 118 return invocationOnMock.callRealMethod(); 119 } 120 } 121