OLD | NEW |
---|---|
(Empty) | |
1 // Copyright 2017 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 #include <memory> | |
6 | |
7 #include "base/macros.h" | |
8 #include "chrome/browser/extensions/chrome_app_icon.h" | |
9 #include "chrome/browser/extensions/chrome_app_icon_delegate.h" | |
10 #include "chrome/browser/extensions/chrome_app_icon_loader.h" | |
11 #include "chrome/browser/extensions/chrome_app_icon_service.h" | |
12 #include "chrome/browser/extensions/extension_service.h" | |
13 #include "chrome/browser/extensions/extension_service_test_base.h" | |
14 #include "chrome/browser/profiles/profile.h" | |
15 #include "chrome/browser/ui/app_icon_loader_delegate.h" | |
16 #include "chrome/common/chrome_constants.h" | |
17 #include "chrome/test/base/testing_profile.h" | |
18 #include "extensions/common/constants.h" | |
19 #include "ui/app_list/app_list_item.h" | |
20 #include "ui/gfx/image/image_unittest_util.h" | |
21 | |
22 #if defined(OS_CHROMEOS) | |
23 #include "chrome/browser/chromeos/arc/arc_util.h" | |
24 #include "chrome/browser/ui/app_list/arc/arc_app_test.h" | |
25 #include "chrome/browser/ui/app_list/extension_app_model_builder.h" | |
26 #include "chrome/browser/ui/app_list/search/extension_app_result.h" | |
27 #include "chrome/browser/ui/app_list/test/test_app_list_controller_delegate.h" | |
28 #include "components/arc/test/fake_app_instance.h" | |
29 #endif // defined(OS_CHROMEOS) | |
30 | |
31 namespace extensions { | |
32 | |
33 namespace { | |
34 | |
35 constexpr char kTestAppId[] = "emfkafnhnpcmabnnkckkchdilgeoekbo"; | |
36 | |
37 // Receives icon image updates from ChromeAppIcon. | |
38 class TestAppIcon : public ChromeAppIconDelegate { | |
39 public: | |
40 TestAppIcon(content::BrowserContext* context, | |
41 const std::string& app_id, | |
42 int size) { | |
43 app_icon_ = | |
44 ChromeAppIconService::Get(context)->CreateIcon(this, app_id, size); | |
45 DCHECK(app_icon_); | |
46 } | |
47 | |
48 ~TestAppIcon() override = default; | |
49 | |
50 void Reset() { app_icon_.reset(); } | |
51 | |
52 int GetIconUpdateCountAndReset() { | |
53 int icon_update_count = icon_update_count_; | |
54 icon_update_count_ = 0; | |
55 return icon_update_count; | |
56 } | |
57 | |
58 size_t icon_update_count() const { return icon_update_count_; } | |
59 ChromeAppIcon* app_icon() { return app_icon_.get(); } | |
60 const gfx::ImageSkia& image_skia() const { return app_icon_->image_skia(); } | |
61 | |
62 void WaitForIconUpdates() { | |
63 base::RunLoop run_loop; | |
64 icon_update_count_expected_ = | |
65 icon_update_count_ + image_skia().image_reps().size(); | |
66 icon_updated_callback_ = run_loop.QuitClosure(); | |
67 run_loop.Run(); | |
68 } | |
69 | |
70 private: | |
71 void OnIconUpdated(ChromeAppIcon* icon) override { | |
72 ++icon_update_count_; | |
73 if ((icon_update_count_ == icon_update_count_expected_) && | |
msw
2017/05/10 19:05:21
nit: extra parens not needed here or on the next l
khmel
2017/05/11 02:13:48
Done.
| |
74 (!icon_updated_callback_.is_null())) { | |
75 base::ResetAndReturn(&icon_updated_callback_).Run(); | |
76 } | |
77 } | |
78 | |
79 std::unique_ptr<ChromeAppIcon> app_icon_; | |
80 | |
81 size_t icon_update_count_ = 0; | |
82 size_t icon_update_count_expected_ = 0; | |
83 | |
84 base::OnceClosure icon_updated_callback_; | |
85 | |
86 DISALLOW_COPY_AND_ASSIGN(TestAppIcon); | |
87 }; | |
88 | |
89 // Receives icon image updates from ChromeAppIconLoader. | |
90 class TestAppIconLoader : public AppIconLoaderDelegate { | |
91 public: | |
92 TestAppIconLoader() = default; | |
93 ~TestAppIconLoader() override = default; | |
94 | |
95 // AppIconLoaderDelegate: | |
96 void OnAppImageUpdated(const std::string& app_id, | |
97 const gfx::ImageSkia& image) override { | |
98 image_skia_ = image; | |
99 } | |
100 | |
101 gfx::ImageSkia& icon() { return image_skia_; } | |
102 const gfx::ImageSkia& icon() const { return image_skia_; } | |
103 | |
104 private: | |
105 gfx::ImageSkia image_skia_; | |
106 | |
107 DISALLOW_COPY_AND_ASSIGN(TestAppIconLoader); | |
108 }; | |
109 | |
110 // Returns true if provided |image| consists from only empty pixels. | |
111 bool IsBlankImage(const gfx::ImageSkia& image) { | |
112 if (!image.width() || !image.height()) | |
113 return false; | |
114 | |
115 const SkBitmap* bitmap = image.bitmap(); | |
116 DCHECK(bitmap); | |
117 DCHECK_EQ(bitmap->width(), image.width()); | |
118 DCHECK_EQ(bitmap->height(), image.height()); | |
119 | |
120 for (int x = 0; x < bitmap->width(); ++x) { | |
121 for (int y = 0; y < bitmap->height(); ++y) { | |
122 if (*bitmap->getAddr32(x, y)) | |
123 return false; | |
124 } | |
125 } | |
126 return true; | |
127 } | |
128 | |
129 // Returns true if provided |image| is grayscale. | |
130 bool IsGrayscaleImage(const gfx::ImageSkia& image) { | |
131 const SkBitmap* bitmap = image.bitmap(); | |
132 DCHECK(bitmap); | |
133 for (int x = 0; x < bitmap->width(); ++x) { | |
134 for (int y = 0; y < bitmap->height(); ++y) { | |
135 const unsigned int pixel = *bitmap->getAddr32(x, y); | |
136 if ((pixel & 0xff) != ((pixel >> 8) & 0xff) || | |
137 (pixel & 0xff) != ((pixel >> 16) & 0xff)) { | |
138 return false; | |
139 } | |
140 } | |
141 } | |
142 return true; | |
143 } | |
144 | |
145 // Returns true if provided |image1| and |image2| are equal. | |
146 bool AreEqual(const gfx::ImageSkia& image1, const gfx::ImageSkia& image2) { | |
147 return gfx::test::AreImagesEqual(gfx::Image(image1), gfx::Image(image2)); | |
148 } | |
149 | |
150 #if defined(OS_CHROMEOS) | |
151 | |
152 bool AreAllImageRepresentationsDifferent(const gfx::ImageSkia& image1, | |
153 const gfx::ImageSkia& image2) { | |
154 const std::vector<gfx::ImageSkiaRep> image1_reps = image1.image_reps(); | |
155 const std::vector<gfx::ImageSkiaRep> image2_reps = image2.image_reps(); | |
156 DCHECK_EQ(image1_reps.size(), image2_reps.size()); | |
157 for (size_t i = 0; i < image1_reps.size(); ++i) { | |
158 const float scale = image1_reps[i].scale(); | |
159 const gfx::ImageSkiaRep& image_rep2 = image2.GetRepresentation(scale); | |
160 if (gfx::test::AreBitmapsClose(image1_reps[i].sk_bitmap(), | |
161 image_rep2.sk_bitmap(), 0)) { | |
162 return false; | |
163 } | |
164 } | |
165 return true; | |
166 } | |
167 | |
168 // Waits until icon content of the item is changed for all representations. | |
169 template <typename T> | |
170 void WaitForIconUpdates(const T& item) { | |
171 item.icon().EnsureRepsForSupportedScales(); | |
172 std::unique_ptr<gfx::ImageSkia> reference_image = item.icon().DeepCopy(); | |
173 while (!AreAllImageRepresentationsDifferent(*reference_image, item.icon())) | |
174 base::RunLoop().RunUntilIdle(); | |
175 } | |
176 | |
177 #endif // defined(OS_CHROMEOS) | |
178 | |
179 } // namespace | |
180 | |
181 class ChromeAppIconTest : public ExtensionServiceTestBase { | |
182 public: | |
183 ChromeAppIconTest() = default; | |
184 ~ChromeAppIconTest() override = default; | |
185 | |
186 // ExtensionServiceTestBase: | |
187 void SetUp() override { | |
188 ExtensionServiceTestBase::SetUp(); | |
189 | |
190 const base::FilePath source_install_dir = | |
191 data_dir().AppendASCII("app_list").AppendASCII("Extensions"); | |
192 const base::FilePath pref_path = | |
193 source_install_dir.DirName().Append(chrome::kPreferencesFilename); | |
194 InitializeInstalledExtensionService(pref_path, source_install_dir); | |
195 service_->Init(); | |
196 } | |
197 | |
198 private: | |
199 DISALLOW_COPY_AND_ASSIGN(ChromeAppIconTest); | |
200 }; | |
201 | |
202 TEST_F(ChromeAppIconTest, IconLifeCycle) { | |
203 TestAppIcon reference_icon(profile(), kTestAppId, | |
204 extension_misc::EXTENSION_ICON_MEDIUM); | |
205 EXPECT_EQ(1U, reference_icon.icon_update_count()); | |
206 // By default no representation in image. | |
207 EXPECT_FALSE(reference_icon.image_skia().HasRepresentation(1.0f)); | |
208 | |
209 // Default blank image must be provided without an update. | |
210 EXPECT_FALSE(reference_icon.image_skia().GetRepresentation(1.0f).is_null()); | |
211 EXPECT_EQ(1U, reference_icon.icon_update_count()); | |
212 EXPECT_TRUE(reference_icon.image_skia().HasRepresentation(1.0f)); | |
213 EXPECT_EQ(extension_misc::EXTENSION_ICON_MEDIUM, | |
214 reference_icon.image_skia().width()); | |
215 EXPECT_EQ(extension_misc::EXTENSION_ICON_MEDIUM, | |
216 reference_icon.image_skia().height()); | |
217 EXPECT_TRUE(IsBlankImage(reference_icon.image_skia())); | |
218 | |
219 // Wait until real image is loaded. | |
220 reference_icon.WaitForIconUpdates(); | |
221 EXPECT_EQ(2U, reference_icon.icon_update_count()); | |
222 EXPECT_FALSE(IsBlankImage(reference_icon.image_skia())); | |
223 EXPECT_FALSE(IsGrayscaleImage(reference_icon.image_skia())); | |
224 | |
225 const gfx::ImageSkia image_before_disable = reference_icon.image_skia(); | |
226 // Disable extension. This should update icon and provide grayed image inline. | |
227 // Note update might be sent twice in CrOS. | |
228 service()->DisableExtension(kTestAppId, Extension::DISABLE_CORRUPTED); | |
229 const size_t update_count_after_disable = reference_icon.icon_update_count(); | |
230 EXPECT_NE(2U, update_count_after_disable); | |
231 EXPECT_FALSE(IsBlankImage(reference_icon.image_skia())); | |
232 EXPECT_TRUE(IsGrayscaleImage(reference_icon.image_skia())); | |
233 | |
234 // Reenable extension. It should match previous enabled image | |
235 service()->EnableExtension(kTestAppId); | |
236 EXPECT_NE(update_count_after_disable, reference_icon.icon_update_count()); | |
237 EXPECT_TRUE(AreEqual(reference_icon.image_skia(), image_before_disable)); | |
238 } | |
239 | |
240 // Validates that icon release is safe. | |
241 TEST_F(ChromeAppIconTest, IconRelease) { | |
242 TestAppIcon test_icon1(profile(), kTestAppId, | |
243 extension_misc::EXTENSION_ICON_MEDIUM); | |
244 TestAppIcon test_icon2(profile(), kTestAppId, | |
245 extension_misc::EXTENSION_ICON_MEDIUM); | |
246 EXPECT_FALSE(test_icon1.image_skia().GetRepresentation(1.0f).is_null()); | |
247 EXPECT_FALSE(test_icon2.image_skia().GetRepresentation(1.0f).is_null()); | |
248 | |
249 // Reset before service is stopped. | |
250 test_icon1.Reset(); | |
251 | |
252 // Reset after service is stopped. | |
253 profile_.reset(); | |
254 test_icon2.Reset(); | |
255 } | |
256 | |
257 #if defined(OS_CHROMEOS) | |
258 | |
259 class ChromeAppIconWithModelTest : public ChromeAppIconTest { | |
260 public: | |
261 ChromeAppIconWithModelTest() = default; | |
262 ~ChromeAppIconWithModelTest() override = default; | |
263 | |
264 protected: | |
265 void CreateBuilder() { | |
266 model_.reset(new app_list::AppListModel); | |
267 controller_.reset(new test::TestAppListControllerDelegate); | |
268 builder_.reset(new ExtensionAppModelBuilder(controller_.get())); | |
269 builder_->InitializeWithProfile(profile(), model_.get()); | |
270 } | |
271 | |
272 void ResetBuilder() { | |
273 builder_.reset(); | |
274 controller_.reset(); | |
275 model_.reset(); | |
276 } | |
277 | |
278 app_list::AppListItem* FindAppListItem(const std::string& app_id) { | |
279 return model_->FindItem(app_id); | |
280 } | |
281 | |
282 test::TestAppListControllerDelegate* app_list_controller() { | |
283 return controller_.get(); | |
284 } | |
285 | |
286 private: | |
287 std::unique_ptr<app_list::AppListModel> model_; | |
288 std::unique_ptr<test::TestAppListControllerDelegate> controller_; | |
289 std::unique_ptr<ExtensionAppModelBuilder> builder_; | |
290 | |
291 DISALLOW_COPY_AND_ASSIGN(ChromeAppIconWithModelTest); | |
292 }; | |
293 | |
294 // Validates that icons are the same for different consumers. | |
295 TEST_F(ChromeAppIconWithModelTest, IconsTheSame) { | |
296 CreateBuilder(); | |
297 | |
298 // App list item is already created. Wait until all image representations are | |
299 // updated and take image snapshot. | |
300 app_list::AppListItem* app_list_item = FindAppListItem(kTestAppId); | |
301 ASSERT_TRUE(app_list_item); | |
302 WaitForIconUpdates<app_list::AppListItem>(*app_list_item); | |
303 std::unique_ptr<gfx::ImageSkia> app_list_item_image = | |
304 app_list_item->icon().DeepCopy(); | |
305 | |
306 // Load reference icon. | |
307 TestAppIcon reference_icon(profile(), kTestAppId, | |
308 extension_misc::EXTENSION_ICON_MEDIUM); | |
309 | |
310 // Wait until reference data is loaded. | |
311 reference_icon.image_skia().EnsureRepsForSupportedScales(); | |
312 reference_icon.WaitForIconUpdates(); | |
313 EXPECT_FALSE(IsBlankImage(reference_icon.image_skia())); | |
314 | |
315 // Now compare with app list icon snapshot. | |
316 EXPECT_TRUE(AreEqual(reference_icon.image_skia(), *app_list_item_image)); | |
317 | |
318 app_list::ExtensionAppResult search(profile(), kTestAppId, | |
319 app_list_controller(), true); | |
320 WaitForIconUpdates<app_list::ExtensionAppResult>(search); | |
321 EXPECT_TRUE(AreEqual(reference_icon.image_skia(), search.icon())); | |
322 | |
323 TestAppIconLoader loader_delegate; | |
324 ChromeAppIconLoader loader(profile(), extension_misc::EXTENSION_ICON_MEDIUM, | |
325 &loader_delegate); | |
326 loader.FetchImage(kTestAppId); | |
327 WaitForIconUpdates<TestAppIconLoader>(loader_delegate); | |
328 EXPECT_TRUE(AreEqual(reference_icon.image_skia(), loader_delegate.icon())); | |
329 | |
330 ResetBuilder(); | |
331 } | |
332 | |
333 TEST_F(ChromeAppIconTest, ChromeBadging) { | |
334 ArcAppTest arc_test; | |
335 arc_test.SetUp(profile()); | |
336 | |
337 TestAppIcon reference_icon(profile(), kTestAppId, | |
338 extension_misc::EXTENSION_ICON_MEDIUM); | |
339 // Wait until reference data is loaded. | |
340 EXPECT_FALSE(reference_icon.image_skia().GetRepresentation(1.0f).is_null()); | |
341 reference_icon.WaitForIconUpdates(); | |
342 EXPECT_FALSE(IsBlankImage(reference_icon.image_skia())); | |
343 | |
344 reference_icon.GetIconUpdateCountAndReset(); | |
345 const gfx::ImageSkia image_before_badging = reference_icon.image_skia(); | |
346 | |
347 // Badging should be applied once package is installed. | |
348 arc_test.app_instance()->RefreshAppList(); | |
349 std::vector<arc::mojom::AppInfo> fake_apps = arc_test.fake_apps(); | |
350 fake_apps[0].package_name = arc_test.fake_packages()[0].package_name; | |
351 arc_test.app_instance()->SendRefreshAppList(fake_apps); | |
352 arc_test.app_instance()->SendRefreshPackageList(arc_test.fake_packages()); | |
353 EXPECT_EQ(1U, reference_icon.icon_update_count()); | |
354 EXPECT_FALSE(AreEqual(reference_icon.image_skia(), image_before_badging)); | |
355 | |
356 // Opts out the Play Store. Badge should be gone and icon image is the same | |
357 // as it was before badging. | |
358 arc::SetArcPlayStoreEnabledForProfile(profile(), false); | |
359 EXPECT_EQ(2U, reference_icon.icon_update_count()); | |
360 EXPECT_TRUE(AreEqual(reference_icon.image_skia(), image_before_badging)); | |
361 } | |
362 | |
363 #endif // defined(OS_CHROMEOS) | |
364 | |
365 } // namespace extensions | |
OLD | NEW |