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