1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.theme.icon;
18 
19 import static junit.framework.Assert.fail;
20 
21 import static org.junit.Assert.assertEquals;
22 
23 import android.annotation.DrawableRes;
24 import android.content.Context;
25 import android.content.pm.ApplicationInfo;
26 import android.content.pm.PackageManager;
27 import android.content.res.Resources;
28 import android.content.res.TypedArray;
29 import android.content.res.XmlResourceParser;
30 import android.text.TextUtils;
31 import android.util.TypedValue;
32 
33 import androidx.test.InstrumentationRegistry;
34 import androidx.test.filters.MediumTest;
35 import androidx.test.runner.AndroidJUnit4;
36 
37 import com.android.internal.util.XmlUtils;
38 
39 import org.junit.Before;
40 import org.junit.Test;
41 import org.junit.runner.RunWith;
42 import org.xmlpull.v1.XmlPullParserException;
43 
44 import java.io.IOException;
45 import java.util.ArrayList;
46 import java.util.Enumeration;
47 import java.util.List;
48 import java.util.zip.ZipEntry;
49 import java.util.zip.ZipFile;
50 
51 @RunWith(AndroidJUnit4.class)
52 @MediumTest
53 public class IconPackOverlayTest {
54     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
55     private static final String[] SYSTEMUI_ICON_PACK_OVERLAY_PACKAGES = {
56             "com.android.theme.icon_pack.circular.systemui",
57             "com.android.theme.icon_pack.rounded.systemui",
58             "com.android.theme.icon_pack.filled.systemui",
59     };
60     private static final String ANDROID_PACKAGE = "android";
61     private static final String[] ANDROID_ICON_PACK_OVERLAY_PACKAGES = {
62             "com.android.theme.icon_pack.circular.android",
63             "com.android.theme.icon_pack.rounded.android",
64             "com.android.theme.icon_pack.filled.android",
65     };
66     private static final String SETTINGS_PACKAGE = "com.android.settings";
67     private static final String[] SETTINGS_ICON_PACK_OVERLAY_PACKAGES = {
68             "com.android.theme.icon_pack.circular.settings",
69             "com.android.theme.icon_pack.rounded.settings",
70             "com.android.theme.icon_pack.filled.settings",
71     };
72 
73     private static final int[] VECTOR_ATTRIBUTES = {
74             android.R.attr.tint,
75             android.R.attr.height,
76             android.R.attr.width,
77             android.R.attr.alpha,
78             android.R.attr.autoMirrored,
79     };
80 
81     private final TypedValue mTargetTypedValue = new TypedValue();
82     private final TypedValue mOverlayTypedValue = new TypedValue();
83     private Context mContext;
84 
85     @Before
setup()86     public void setup() {
87         mContext = InstrumentationRegistry.getContext();
88     }
89 
90     /**
91      * Ensure that drawable icons in icon packs targeting android have corresponding underlying
92      * drawables in android. This test fails if you remove/rename an overlaid icon in android.
93      * If so, make the same change to the corresponding drawables in the overlay packages.
94      */
95     @Test
testAndroidFramework_containsAllOverlayedIcons()96     public void testAndroidFramework_containsAllOverlayedIcons() {
97         containsAllOverlayedIcons(ANDROID_PACKAGE, ANDROID_ICON_PACK_OVERLAY_PACKAGES);
98     }
99 
100     /**
101      * Ensure that drawable icons in icon packs targeting settings have corresponding underlying
102      * drawables in settings. This test fails if you remove/rename an overlaid icon in settings.
103      * If so, make the same change to the corresponding drawables in the overlay packages.
104      */
105     @Test
testSettings_containsAllOverlayedIcons()106     public void testSettings_containsAllOverlayedIcons() {
107         containsAllOverlayedIcons(SETTINGS_PACKAGE, SETTINGS_ICON_PACK_OVERLAY_PACKAGES);
108     }
109 
110     /**
111      * Ensure that drawable icons in icon packs targeting systemui have corresponding underlying
112      * drawables in systemui. This test fails if you remove/rename an overlaid icon in systemui.
113      * If so, make the same change to the corresponding drawables in the overlay packages.
114      */
115     @Test
testSystemUI_containAllOverlayedIcons()116     public void testSystemUI_containAllOverlayedIcons() {
117         containsAllOverlayedIcons(SYSTEMUI_PACKAGE, SYSTEMUI_ICON_PACK_OVERLAY_PACKAGES);
118     }
119 
120     /**
121      * Ensures that all overlay icons have the same values for {@link #VECTOR_ATTRIBUTES} as the
122      * underlying drawable in android. To fix this test, make the attribute change to all of the
123      * corresponding drawables in the overlay packages.
124      */
125     @Test
testAndroidFramework_hasEqualVectorDrawableAttributes()126     public void testAndroidFramework_hasEqualVectorDrawableAttributes() {
127         hasEqualVectorDrawableAttributes(ANDROID_PACKAGE, ANDROID_ICON_PACK_OVERLAY_PACKAGES);
128     }
129 
130     /**
131      * Ensures that all overlay icons have the same values for {@link #VECTOR_ATTRIBUTES} as the
132      * underlying drawable in settings. To fix this test, make the attribute change to all of the
133      * corresponding drawables in the overlay packages.
134      */
135     @Test
testSettings_hasEqualVectorDrawableAttributes()136     public void testSettings_hasEqualVectorDrawableAttributes() {
137         hasEqualVectorDrawableAttributes(SETTINGS_PACKAGE, SETTINGS_ICON_PACK_OVERLAY_PACKAGES);
138     }
139 
140     /**
141      * Ensures that all overlay icons have the same values for {@link #VECTOR_ATTRIBUTES} as the
142      * underlying drawable in systemui. To fix this test, make the attribute change to all of the
143      * corresponding drawables in the overlay packages.
144      */
145     @Test
testSystemUI_hasEqualVectorDrawableAttributes()146     public void testSystemUI_hasEqualVectorDrawableAttributes() {
147         hasEqualVectorDrawableAttributes(SYSTEMUI_PACKAGE, SYSTEMUI_ICON_PACK_OVERLAY_PACKAGES);
148     }
149 
containsAllOverlayedIcons(String targetPkg, String[] overlayPkgs)150     private void containsAllOverlayedIcons(String targetPkg, String[] overlayPkgs) {
151         final Resources targetResources;
152         try {
153             targetResources = mContext.getPackageManager()
154                     .getResourcesForApplication(targetPkg);
155         } catch (PackageManager.NameNotFoundException e) {
156             return; // No need to test overlays if target package does not exist on the system.
157         }
158 
159         StringBuilder errors = new StringBuilder();
160         for (String overlayPackage : overlayPkgs) {
161             final ApplicationInfo info;
162             try {
163                 info = mContext.getPackageManager().getApplicationInfo(overlayPackage, 0);
164             } catch (PackageManager.NameNotFoundException e) {
165                 continue; // No need to test overlay resources if apk is not on the system.
166             }
167             final List<String> iconPackDrawables = getDrawablesFromOverlay(info);
168             for (int i = 0; i < iconPackDrawables.size(); i++) {
169                 String resourceName = iconPackDrawables.get(i);
170                 int targetRid = targetResources.getIdentifier(resourceName, "drawable", targetPkg);
171                 if (targetRid == Resources.ID_NULL) {
172                     errors.append(String.format("[%s] is not contained in the target package [%s]",
173                             resourceName, targetPkg));
174                 }
175             }
176         }
177 
178         if (!TextUtils.isEmpty(errors)) {
179             fail(errors.toString());
180         }
181     }
182 
hasEqualVectorDrawableAttributes(String targetPkg, String[] overlayPackages)183     private void hasEqualVectorDrawableAttributes(String targetPkg, String[] overlayPackages) {
184         final Resources targetRes;
185         try {
186             targetRes = mContext.getPackageManager().getResourcesForApplication(targetPkg);
187         } catch (PackageManager.NameNotFoundException e) {
188             return; // No need to test overlays if target package does not exist on the system.
189         }
190 
191         StringBuilder errors = new StringBuilder();
192 
193         for (String overlayPkg : overlayPackages) {
194             final ApplicationInfo info;
195             try {
196                 info = mContext.getPackageManager().getApplicationInfo(overlayPkg, 0);
197             } catch (PackageManager.NameNotFoundException e) {
198                 continue; // No need to test overlay resources if apk is not on the system.
199             }
200             final List<String> iconPackDrawables = getDrawablesFromOverlay(info);
201             final Resources overlayRes;
202             try {
203                 overlayRes = mContext.getPackageManager().getResourcesForApplication(overlayPkg);
204             } catch (PackageManager.NameNotFoundException e) {
205                 continue; // No need to test overlay resources if apk is not on the system.
206             }
207 
208             for (int i = 0; i < iconPackDrawables.size(); i++) {
209                 String resourceName = iconPackDrawables.get(i);
210                 int targetRid = targetRes.getIdentifier(resourceName, "drawable", targetPkg);
211                 int overlayRid = overlayRes.getIdentifier(resourceName, "drawable", overlayPkg);
212                 TypedArray targetAttrs = getAVDAttributes(targetRes, targetRid);
213                 if (targetAttrs == null) {
214                     errors.append(String.format(
215                             "[%s] in pkg [%s] does not exist or is not a valid vector drawable.\n",
216                             resourceName, targetPkg));
217                     continue;
218                 }
219 
220                 TypedArray overlayAttrs = getAVDAttributes(overlayRes, overlayRid);
221                 if (overlayAttrs == null) {
222                     errors.append(String.format(
223                             "[%s] in pkg [%s] does not exist or is not a valid vector drawable.\n",
224                             resourceName, overlayPkg));
225                     continue;
226                 }
227 
228                 if (!attributesEquals(targetAttrs, overlayAttrs)) {
229                     errors.append(String.format("[drawable/%s] in [%s] does not have the same "
230                                     + "attributes as the corresponding drawable from [%s]\n",
231                             resourceName, targetPkg, overlayPkg));
232                 }
233                 targetAttrs.recycle();
234                 overlayAttrs.recycle();
235             }
236         }
237 
238         if (!TextUtils.isEmpty(errors)) {
239             fail(errors.toString());
240         }
241     }
242 
getAVDAttributes(Resources resources, @DrawableRes int rid)243     private TypedArray getAVDAttributes(Resources resources, @DrawableRes int rid) {
244         try {
245             XmlResourceParser parser = resources.getXml(rid);
246             XmlUtils.nextElement(parser);
247             // Always use the the test apk theme to resolve attributes.
248             return mContext.getTheme().obtainStyledAttributes(parser, VECTOR_ATTRIBUTES, 0, 0);
249         } catch (XmlPullParserException | IOException  | Resources.NotFoundException e) {
250             return null;
251         }
252     }
253 
attributesEquals(TypedArray target, TypedArray overlay)254     private boolean attributesEquals(TypedArray target, TypedArray overlay) {
255         assertEquals(target.length(), overlay.length());
256         for (int i = 0; i < target.length(); i++) {
257             target.getValue(i, mTargetTypedValue);
258             overlay.getValue(i, mOverlayTypedValue);
259             if (!attributesEquals(mTargetTypedValue, mOverlayTypedValue)) {
260                 return false;
261             }
262         }
263         return true;
264     }
265 
attributesEquals(TypedValue target, TypedValue overlay)266     private static boolean attributesEquals(TypedValue target, TypedValue overlay) {
267         return target.type == overlay.type && target.data == overlay.data;
268     }
269 
getDrawablesFromOverlay(ApplicationInfo applicationInfo)270     private static List<String> getDrawablesFromOverlay(ApplicationInfo applicationInfo) {
271         try {
272             final ArrayList<String> drawables = new ArrayList<>();
273             ZipFile file = new ZipFile(applicationInfo.sourceDir);
274             Enumeration<? extends ZipEntry> entries = file.entries();
275             while (entries.hasMoreElements()) {
276                 ZipEntry element = entries.nextElement();
277                 String name = element.getName();
278                 if (name.contains("/drawable/")) {
279                     name = name.substring(name.lastIndexOf('/') + 1);
280                     if (name.contains(".")) {
281                         name = name.substring(0, name.indexOf('.'));
282                     }
283                     drawables.add(name);
284                 }
285             }
286             return drawables;
287         } catch (IOException e) {
288             fail(String.format("Failed to retrieve drawables from package [%s] with message [%s]",
289                     applicationInfo.packageName, e.getMessage()));
290             return null;
291         }
292     }
293 }
294