1 // Copyright 2018 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 <set>
6 #include <sstream>
7 #include <utility>
8 #include <vector>
9 
10 #include "base/bind.h"
11 #include "base/callback.h"
12 #include "base/files/scoped_temp_dir.h"
13 #include "base/optional.h"
14 #include "base/run_loop.h"
15 #include "components/services/app_service/app_service_impl.h"
16 #include "components/services/app_service/public/cpp/intent_filter_util.h"
17 #include "components/services/app_service/public/cpp/intent_util.h"
18 #include "components/services/app_service/public/cpp/preferred_apps_list.h"
19 #include "components/services/app_service/public/cpp/publisher_base.h"
20 #include "components/services/app_service/public/mojom/types.mojom.h"
21 #include "content/public/test/browser_task_environment.h"
22 #include "mojo/public/cpp/bindings/pending_receiver.h"
23 #include "mojo/public/cpp/bindings/pending_remote.h"
24 #include "mojo/public/cpp/bindings/receiver_set.h"
25 #include "mojo/public/cpp/bindings/remote.h"
26 #include "mojo/public/cpp/bindings/remote_set.h"
27 #include "testing/gtest/include/gtest/gtest.h"
28 
29 namespace apps {
30 
31 class FakePublisher : public apps::PublisherBase {
32  public:
FakePublisher(AppServiceImpl * impl,apps::mojom::AppType app_type,std::vector<std::string> initial_app_ids)33   FakePublisher(AppServiceImpl* impl,
34                 apps::mojom::AppType app_type,
35                 std::vector<std::string> initial_app_ids)
36       : app_type_(app_type), known_app_ids_(std::move(initial_app_ids)) {
37     mojo::PendingRemote<apps::mojom::Publisher> remote;
38     receivers_.Add(this, remote.InitWithNewPipeAndPassReceiver());
39     impl->RegisterPublisher(std::move(remote), app_type_);
40   }
41 
PublishMoreApps(std::vector<std::string> app_ids)42   void PublishMoreApps(std::vector<std::string> app_ids) {
43     for (auto& subscriber : subscribers_) {
44       CallOnApps(subscriber.get(), app_ids, /*uninstall=*/false);
45     }
46     for (const auto& app_id : app_ids) {
47       known_app_ids_.push_back(app_id);
48     }
49   }
50 
UninstallApps(std::vector<std::string> app_ids,AppServiceImpl * impl)51   void UninstallApps(std::vector<std::string> app_ids, AppServiceImpl* impl) {
52     for (auto& subscriber : subscribers_) {
53       CallOnApps(subscriber.get(), app_ids, /*uninstall=*/true);
54     }
55     for (const auto& app_id : app_ids) {
56       known_app_ids_.push_back(app_id);
57       impl->RemovePreferredApp(app_type_, app_id);
58     }
59   }
60 
61   std::string load_icon_app_id;
62 
63  private:
Connect(mojo::PendingRemote<apps::mojom::Subscriber> subscriber_remote,apps::mojom::ConnectOptionsPtr opts)64   void Connect(mojo::PendingRemote<apps::mojom::Subscriber> subscriber_remote,
65                apps::mojom::ConnectOptionsPtr opts) override {
66     mojo::Remote<apps::mojom::Subscriber> subscriber(
67         std::move(subscriber_remote));
68     CallOnApps(subscriber.get(), known_app_ids_, /*uninstall=*/false);
69     subscribers_.Add(std::move(subscriber));
70   }
71 
LoadIcon(const std::string & app_id,apps::mojom::IconKeyPtr icon_key,apps::mojom::IconType icon_type,int32_t size_hint_in_dip,bool allow_placeholder_icon,LoadIconCallback callback)72   void LoadIcon(const std::string& app_id,
73                 apps::mojom::IconKeyPtr icon_key,
74                 apps::mojom::IconType icon_type,
75                 int32_t size_hint_in_dip,
76                 bool allow_placeholder_icon,
77                 LoadIconCallback callback) override {
78     load_icon_app_id = app_id;
79     std::move(callback).Run(apps::mojom::IconValue::New());
80   }
81 
Launch(const std::string & app_id,int32_t event_flags,apps::mojom::LaunchSource launch_source,int64_t display_id)82   void Launch(const std::string& app_id,
83               int32_t event_flags,
84               apps::mojom::LaunchSource launch_source,
85               int64_t display_id) override {}
86 
CallOnApps(apps::mojom::Subscriber * subscriber,std::vector<std::string> & app_ids,bool uninstall)87   void CallOnApps(apps::mojom::Subscriber* subscriber,
88                   std::vector<std::string>& app_ids,
89                   bool uninstall) {
90     std::vector<apps::mojom::AppPtr> apps;
91     for (const auto& app_id : app_ids) {
92       auto app = apps::mojom::App::New();
93       app->app_type = app_type_;
94       app->app_id = app_id;
95       if (uninstall) {
96         app->readiness = apps::mojom::Readiness::kUninstalledByUser;
97       }
98       apps.push_back(std::move(app));
99     }
100     subscriber->OnApps(std::move(apps));
101   }
102 
103   apps::mojom::AppType app_type_;
104   std::vector<std::string> known_app_ids_;
105   mojo::ReceiverSet<apps::mojom::Publisher> receivers_;
106   mojo::RemoteSet<apps::mojom::Subscriber> subscribers_;
107 };
108 
109 class FakeSubscriber : public apps::mojom::Subscriber {
110  public:
FakeSubscriber(AppServiceImpl * impl)111   explicit FakeSubscriber(AppServiceImpl* impl) {
112     mojo::PendingRemote<apps::mojom::Subscriber> remote;
113     receivers_.Add(this, remote.InitWithNewPipeAndPassReceiver());
114     impl->RegisterSubscriber(std::move(remote), nullptr);
115   }
116 
AppIdsSeen()117   std::string AppIdsSeen() {
118     std::stringstream ss;
119     for (const auto& app_id : app_ids_seen_) {
120       ss << app_id;
121     }
122     return ss.str();
123   }
124 
PreferredApps()125   PreferredAppsList& PreferredApps() { return preferred_apps_; }
126 
127  private:
OnApps(std::vector<apps::mojom::AppPtr> deltas)128   void OnApps(std::vector<apps::mojom::AppPtr> deltas) override {
129     for (const auto& delta : deltas) {
130       app_ids_seen_.insert(delta->app_id);
131       if (delta->readiness == apps::mojom::Readiness::kUninstalledByUser) {
132         preferred_apps_.DeleteAppId(delta->app_id);
133       }
134     }
135   }
136 
Clone(mojo::PendingReceiver<apps::mojom::Subscriber> receiver)137   void Clone(mojo::PendingReceiver<apps::mojom::Subscriber> receiver) override {
138     receivers_.Add(this, std::move(receiver));
139   }
140 
OnPreferredAppSet(const std::string & app_id,apps::mojom::IntentFilterPtr intent_filter)141   void OnPreferredAppSet(const std::string& app_id,
142                          apps::mojom::IntentFilterPtr intent_filter) override {
143     preferred_apps_.AddPreferredApp(app_id, intent_filter);
144   }
145 
OnPreferredAppRemoved(const std::string & app_id,apps::mojom::IntentFilterPtr intent_filter)146   void OnPreferredAppRemoved(
147       const std::string& app_id,
148       apps::mojom::IntentFilterPtr intent_filter) override {
149     preferred_apps_.DeletePreferredApp(app_id, intent_filter);
150   }
151 
InitializePreferredApps(PreferredAppsList::PreferredApps preferred_apps)152   void InitializePreferredApps(
153       PreferredAppsList::PreferredApps preferred_apps) override {
154     preferred_apps_.Init(preferred_apps);
155   }
156 
157   mojo::ReceiverSet<apps::mojom::Subscriber> receivers_;
158   std::set<std::string> app_ids_seen_;
159   apps::PreferredAppsList preferred_apps_;
160 };
161 
162 class AppServiceImplTest : public testing::Test {
163  protected:
164   // base::test::TaskEnvironment task_environment_;
165   content::BrowserTaskEnvironment task_environment_;
166   base::ScopedTempDir temp_dir_;
167 };
168 
TEST_F(AppServiceImplTest,PubSub)169 TEST_F(AppServiceImplTest, PubSub) {
170   const int size_hint_in_dip = 64;
171 
172   ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
173   AppServiceImpl impl(temp_dir_.GetPath(),
174                       /*is_share_intents_supported=*/false);
175 
176   // Start with one subscriber.
177   FakeSubscriber sub0(&impl);
178   impl.FlushMojoCallsForTesting();
179   EXPECT_EQ("", sub0.AppIdsSeen());
180 
181   // Add one publisher.
182   FakePublisher pub0(&impl, apps::mojom::AppType::kArc,
183                      std::vector<std::string>{"A", "B"});
184   impl.FlushMojoCallsForTesting();
185   EXPECT_EQ("AB", sub0.AppIdsSeen());
186 
187   // Have that publisher publish more apps.
188   pub0.PublishMoreApps(std::vector<std::string>{"C", "D", "E"});
189   impl.FlushMojoCallsForTesting();
190   EXPECT_EQ("ABCDE", sub0.AppIdsSeen());
191 
192   // Add a second publisher.
193   FakePublisher pub1(&impl, apps::mojom::AppType::kBuiltIn,
194                      std::vector<std::string>{"m"});
195   impl.FlushMojoCallsForTesting();
196   EXPECT_EQ("ABCDEm", sub0.AppIdsSeen());
197 
198   // Have both publishers publish more apps.
199   pub0.PublishMoreApps(std::vector<std::string>{"F"});
200   pub1.PublishMoreApps(std::vector<std::string>{"n"});
201   impl.FlushMojoCallsForTesting();
202   EXPECT_EQ("ABCDEFmn", sub0.AppIdsSeen());
203 
204   // Add a second subscriber.
205   FakeSubscriber sub1(&impl);
206   impl.FlushMojoCallsForTesting();
207   EXPECT_EQ("ABCDEFmn", sub0.AppIdsSeen());
208   EXPECT_EQ("ABCDEFmn", sub1.AppIdsSeen());
209 
210   // Publish more apps.
211   pub1.PublishMoreApps(std::vector<std::string>{"o", "p", "q"});
212   impl.FlushMojoCallsForTesting();
213   EXPECT_EQ("ABCDEFmnopq", sub0.AppIdsSeen());
214   EXPECT_EQ("ABCDEFmnopq", sub1.AppIdsSeen());
215 
216   // Add a third publisher.
217   FakePublisher pub2(&impl, apps::mojom::AppType::kCrostini,
218                      std::vector<std::string>{"$"});
219   impl.FlushMojoCallsForTesting();
220   EXPECT_EQ("$ABCDEFmnopq", sub0.AppIdsSeen());
221   EXPECT_EQ("$ABCDEFmnopq", sub1.AppIdsSeen());
222 
223   // Publish more apps.
224   pub2.PublishMoreApps(std::vector<std::string>{"&"});
225   pub1.PublishMoreApps(std::vector<std::string>{"r"});
226   pub0.PublishMoreApps(std::vector<std::string>{"G"});
227   impl.FlushMojoCallsForTesting();
228   EXPECT_EQ("$&ABCDEFGmnopqr", sub0.AppIdsSeen());
229   EXPECT_EQ("$&ABCDEFGmnopqr", sub1.AppIdsSeen());
230 
231   // Call LoadIcon on the impl twice.
232   //
233   // The first time (i == 0), it should be forwarded onto the AppType::kBuiltIn
234   // publisher (which is pub1) and no other publisher.
235   //
236   // The second time (i == 1), passing AppType::kUnknown, none of the
237   // publishers' LoadIcon's should fire, but the callback should still be run.
238   for (int i = 0; i < 2; i++) {
239     auto app_type = i == 0 ? apps::mojom::AppType::kBuiltIn
240                            : apps::mojom::AppType::kUnknown;
241 
242     bool callback_ran = false;
243     pub0.load_icon_app_id = "-";
244     pub1.load_icon_app_id = "-";
245     pub2.load_icon_app_id = "-";
246     auto icon_key = apps::mojom::IconKey::New(0, 0, 0);
247     constexpr bool allow_placeholder_icon = false;
248     impl.LoadIcon(
249         app_type, "o", std::move(icon_key),
250         apps::mojom::IconType::kUncompressed, size_hint_in_dip,
251         allow_placeholder_icon,
252         base::BindOnce(
253             [](bool* ran, apps::mojom::IconValuePtr iv) { *ran = true; },
254             &callback_ran));
255     impl.FlushMojoCallsForTesting();
256     EXPECT_TRUE(callback_ran);
257     EXPECT_EQ("-", pub0.load_icon_app_id);
258     EXPECT_EQ(i == 0 ? "o" : "-", pub1.load_icon_app_id);
259     EXPECT_EQ("-", pub2.load_icon_app_id);
260   }
261 }
262 
263 // TODO(https://crbug.com/1074596) Test to see if the flakiness is fixed. If it
264 // is not fixed, please update to the same bug.
TEST_F(AppServiceImplTest,PreferredApps)265 TEST_F(AppServiceImplTest, PreferredApps) {
266   // Test Initialize.
267   ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
268   AppServiceImpl impl(temp_dir_.GetPath(),
269                       /*is_share_intents_supported=*/false);
270   impl.GetPreferredAppsForTesting().Init();
271 
272   const char kAppId1[] = "abcdefg";
273   const char kAppId2[] = "aaaaaaa";
274   GURL filter_url = GURL("https://www.google.com/abc");
275   auto intent_filter = apps_util::CreateIntentFilterForUrlScope(filter_url);
276 
277   impl.GetPreferredAppsForTesting().AddPreferredApp(kAppId1, intent_filter);
278 
279   // Add one subscriber.
280   FakeSubscriber sub0(&impl);
281   task_environment_.RunUntilIdle();
282   EXPECT_EQ(sub0.PreferredApps().GetValue(),
283             impl.GetPreferredAppsForTesting().GetValue());
284 
285   // Add another subscriber.
286   FakeSubscriber sub1(&impl);
287   task_environment_.RunUntilIdle();
288   EXPECT_EQ(sub1.PreferredApps().GetValue(),
289             impl.GetPreferredAppsForTesting().GetValue());
290 
291   FakePublisher pub0(&impl, apps::mojom::AppType::kArc,
292                      std::vector<std::string>{kAppId1, kAppId2});
293   task_environment_.RunUntilIdle();
294 
295   // Test sync preferred app to all subscribers.
296   filter_url = GURL("https://www.abc.com/");
297   GURL another_filter_url = GURL("https://www.test.com/");
298   intent_filter = apps_util::CreateIntentFilterForUrlScope(filter_url);
299   auto another_intent_filter =
300       apps_util::CreateIntentFilterForUrlScope(another_filter_url);
301 
302   task_environment_.RunUntilIdle();
303   EXPECT_EQ(base::nullopt,
304             sub0.PreferredApps().FindPreferredAppForUrl(filter_url));
305   EXPECT_EQ(base::nullopt,
306             sub1.PreferredApps().FindPreferredAppForUrl(filter_url));
307   EXPECT_EQ(base::nullopt,
308             sub0.PreferredApps().FindPreferredAppForUrl(another_filter_url));
309   EXPECT_EQ(base::nullopt,
310             sub1.PreferredApps().FindPreferredAppForUrl(another_filter_url));
311 
312   impl.AddPreferredApp(
313       apps::mojom::AppType::kUnknown, kAppId2, intent_filter->Clone(),
314       apps_util::CreateIntentFromUrl(filter_url), /*from_publisher=*/true);
315   impl.AddPreferredApp(apps::mojom::AppType::kUnknown, kAppId2,
316                        another_intent_filter->Clone(),
317                        apps_util::CreateIntentFromUrl(another_filter_url),
318                        /*from_publisher=*/true);
319   task_environment_.RunUntilIdle();
320   EXPECT_EQ(kAppId2, sub0.PreferredApps().FindPreferredAppForUrl(filter_url));
321   EXPECT_EQ(kAppId2, sub1.PreferredApps().FindPreferredAppForUrl(filter_url));
322   EXPECT_EQ(kAppId2,
323             sub0.PreferredApps().FindPreferredAppForUrl(another_filter_url));
324   EXPECT_EQ(kAppId2,
325             sub1.PreferredApps().FindPreferredAppForUrl(another_filter_url));
326 
327   // Test that uninstall removes all the settings for the app.
328   pub0.UninstallApps(std::vector<std::string>{kAppId2}, &impl);
329   task_environment_.RunUntilIdle();
330   EXPECT_EQ(base::nullopt,
331             sub0.PreferredApps().FindPreferredAppForUrl(filter_url));
332   EXPECT_EQ(base::nullopt,
333             sub1.PreferredApps().FindPreferredAppForUrl(filter_url));
334   EXPECT_EQ(base::nullopt,
335             sub0.PreferredApps().FindPreferredAppForUrl(another_filter_url));
336   EXPECT_EQ(base::nullopt,
337             sub1.PreferredApps().FindPreferredAppForUrl(another_filter_url));
338 
339   impl.AddPreferredApp(
340       apps::mojom::AppType::kUnknown, kAppId2, intent_filter->Clone(),
341       apps_util::CreateIntentFromUrl(filter_url), /*from_publisher=*/true);
342   impl.AddPreferredApp(apps::mojom::AppType::kUnknown, kAppId2,
343                        another_intent_filter->Clone(),
344                        apps_util::CreateIntentFromUrl(another_filter_url),
345                        /*from_publisher=*/true);
346   task_environment_.RunUntilIdle();
347 
348   EXPECT_EQ(kAppId2, sub0.PreferredApps().FindPreferredAppForUrl(filter_url));
349   EXPECT_EQ(kAppId2, sub1.PreferredApps().FindPreferredAppForUrl(filter_url));
350   EXPECT_EQ(kAppId2,
351             sub0.PreferredApps().FindPreferredAppForUrl(another_filter_url));
352   EXPECT_EQ(kAppId2,
353             sub1.PreferredApps().FindPreferredAppForUrl(another_filter_url));
354 
355   // Test that remove setting for one filter.
356   impl.RemovePreferredAppForFilter(apps::mojom::AppType::kUnknown, kAppId2,
357                                    intent_filter->Clone());
358   task_environment_.RunUntilIdle();
359   EXPECT_EQ(base::nullopt,
360             sub0.PreferredApps().FindPreferredAppForUrl(filter_url));
361   EXPECT_EQ(base::nullopt,
362             sub1.PreferredApps().FindPreferredAppForUrl(filter_url));
363   EXPECT_EQ(kAppId2,
364             sub0.PreferredApps().FindPreferredAppForUrl(another_filter_url));
365   EXPECT_EQ(kAppId2,
366             sub1.PreferredApps().FindPreferredAppForUrl(another_filter_url));
367 }
368 
TEST_F(AppServiceImplTest,PreferredAppsPersistency)369 TEST_F(AppServiceImplTest, PreferredAppsPersistency) {
370   ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
371 
372   const char kAppId1[] = "abcdefg";
373   GURL filter_url = GURL("https://www.google.com/abc");
374   auto intent_filter = apps_util::CreateIntentFilterForUrlScope(filter_url);
375   {
376     base::RunLoop run_loop_read;
377     base::RunLoop run_loop_write;
378     AppServiceImpl impl(temp_dir_.GetPath(),
379                         /*is_share_intents_supported=*/false,
380                         run_loop_read.QuitClosure(),
381                         run_loop_write.QuitClosure());
382     impl.FlushMojoCallsForTesting();
383     run_loop_read.Run();
384     impl.AddPreferredApp(apps::mojom::AppType::kUnknown, kAppId1,
385                          intent_filter->Clone(),
386                          apps_util::CreateIntentFromUrl(filter_url),
387                          /*from_publisher=*/false);
388     run_loop_write.Run();
389     impl.FlushMojoCallsForTesting();
390   }
391   // Create a new impl to initialize preferred apps from the disk.
392   {
393     base::RunLoop run_loop_read;
394     AppServiceImpl impl(temp_dir_.GetPath(),
395                         /*is_share_intents_supported=*/false,
396                         run_loop_read.QuitClosure());
397     impl.FlushMojoCallsForTesting();
398     run_loop_read.Run();
399     EXPECT_EQ(kAppId1, impl.GetPreferredAppsForTesting().FindPreferredAppForUrl(
400                            filter_url));
401   }
402 }
403 
TEST_F(AppServiceImplTest,PreferredAppsUpgrade)404 TEST_F(AppServiceImplTest, PreferredAppsUpgrade) {
405   ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
406 
407   const char kAppId1[] = "abcdefg";
408   const char kAppId2[] = "gfedcba";
409   GURL filter_url1 = GURL("https://www.google.com/abc");
410   GURL filter_url2 = GURL("https://www.abc.com");
411   auto intent_filter1 = apps_util::CreateIntentFilterForUrlScope(filter_url1);
412   auto intent_filter1_with_action = apps_util::CreateIntentFilterForUrlScope(
413       filter_url1, /*with_action_view=*/true);
414   auto intent_filter2_with_action = apps_util::CreateIntentFilterForUrlScope(
415       filter_url2, /*with_action_view=*/true);
416   {
417     base::RunLoop run_loop_read;
418     base::RunLoop run_loop_write;
419     AppServiceImpl impl(temp_dir_.GetPath(),
420                         /*is_share_intents_supported=*/false,
421                         run_loop_read.QuitClosure(),
422                         run_loop_write.QuitClosure());
423     impl.FlushMojoCallsForTesting();
424     run_loop_read.Run();
425     impl.AddPreferredApp(apps::mojom::AppType::kUnknown, kAppId1,
426                          intent_filter1->Clone(),
427                          apps_util::CreateIntentFromUrl(filter_url1),
428                          /*from_publisher=*/false);
429     impl.AddPreferredApp(apps::mojom::AppType::kUnknown, kAppId2,
430                          intent_filter2_with_action->Clone(),
431                          apps_util::CreateIntentFromUrl(filter_url2),
432                          /*from_publisher=*/false);
433     run_loop_write.Run();
434     impl.FlushMojoCallsForTesting();
435 
436     // If try to remove old intent filter with filter with action, it wouldn't
437     // work.
438     impl.RemovePreferredAppForFilter(apps::mojom::AppType::kUnknown, kAppId1,
439                                      intent_filter1_with_action->Clone());
440     task_environment_.RunUntilIdle();
441     EXPECT_EQ(kAppId1, impl.GetPreferredAppsForTesting().FindPreferredAppForUrl(
442                            filter_url1));
443     EXPECT_EQ(kAppId2, impl.GetPreferredAppsForTesting().FindPreferredAppForUrl(
444                            filter_url2));
445   }
446 
447   // Create a new impl with sharing flag on to initialize preferred apps from
448   // the disk.
449   {
450     base::RunLoop run_loop_read;
451     base::RunLoop run_loop_write;
452     AppServiceImpl impl(temp_dir_.GetPath(),
453                         /*is_share_intents_supported=*/true,
454                         run_loop_read.QuitClosure(),
455                         run_loop_write.QuitClosure());
456     impl.FlushMojoCallsForTesting();
457     run_loop_read.Run();
458     EXPECT_EQ(kAppId1, impl.GetPreferredAppsForTesting().FindPreferredAppForUrl(
459                            filter_url1));
460     EXPECT_EQ(kAppId2, impl.GetPreferredAppsForTesting().FindPreferredAppForUrl(
461                            filter_url2));
462     run_loop_write.Run();
463     impl.FlushMojoCallsForTesting();
464   }
465 
466   // Create another new impl to read from disk and see if the filter is upgraded
467   // by trying to delete the entry using new filter.
468   {
469     base::RunLoop run_loop_read;
470     AppServiceImpl impl(temp_dir_.GetPath(),
471                         /*is_share_intents_supported=*/false,
472                         run_loop_read.QuitClosure());
473     impl.FlushMojoCallsForTesting();
474     run_loop_read.Run();
475     impl.RemovePreferredAppForFilter(apps::mojom::AppType::kUnknown, kAppId1,
476                                      intent_filter1_with_action->Clone());
477     impl.RemovePreferredAppForFilter(apps::mojom::AppType::kUnknown, kAppId2,
478                                      intent_filter2_with_action->Clone());
479     task_environment_.RunUntilIdle();
480     EXPECT_EQ(
481         base::nullopt,
482         impl.GetPreferredAppsForTesting().FindPreferredAppForUrl(filter_url1));
483     EXPECT_EQ(
484         base::nullopt,
485         impl.GetPreferredAppsForTesting().FindPreferredAppForUrl(filter_url2));
486   }
487 }
488 
489 }  // namespace apps
490