• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.car.oem;
18 
19 import android.annotation.Nullable;
20 import android.car.builtin.content.pm.PackageManagerHelper;
21 import android.car.builtin.os.BuildHelper;
22 import android.car.builtin.util.Slogf;
23 import android.car.oem.IOemCarAudioDuckingService;
24 import android.car.oem.IOemCarAudioFocusService;
25 import android.car.oem.IOemCarAudioVolumeService;
26 import android.car.oem.IOemCarService;
27 import android.car.oem.IOemCarServiceCallback;
28 import android.content.ComponentName;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.ServiceConnection;
32 import android.content.pm.PackageInfo;
33 import android.content.pm.PackageManager.NameNotFoundException;
34 import android.content.res.Resources;
35 import android.os.Binder;
36 import android.os.Handler;
37 import android.os.HandlerThread;
38 import android.os.IBinder;
39 import android.os.RemoteException;
40 import android.os.SystemClock;
41 import android.os.SystemProperties;
42 import android.os.UserHandle;
43 import android.text.TextUtils;
44 import android.util.Log;
45 
46 import com.android.car.CarServiceBase;
47 import com.android.car.CarServiceUtils;
48 import com.android.car.R;
49 import com.android.car.internal.util.IndentingPrintWriter;
50 import com.android.internal.annotations.GuardedBy;
51 import com.android.internal.annotations.VisibleForTesting;
52 
53 import java.util.ArrayList;
54 import java.util.concurrent.CountDownLatch;
55 import java.util.concurrent.TimeUnit;
56 import java.util.concurrent.TimeoutException;
57 
58 /**
59  * Manages access to OemCarService.
60  *
61  * <p>All calls in this class are blocking on OEM service initialization, so should be called as
62  *  late as possible.
63  *
64  * <b>NOTE</b>: All {@link CarOemProxyService} call should be after init of ICarImpl. If any
65  * component calls {@link CarOemProxyService} before init of ICarImpl complete, it would throw
66  * {@link IllegalStateException}.
67  */
68 public final class CarOemProxyService implements CarServiceBase {
69 
70     private static final String TAG = CarOemProxyService.class.getSimpleName();
71     private static final String CALL_TAG = CarOemProxyService.class.getSimpleName();
72     private static final boolean DBG = Slogf.isLoggable(TAG, Log.DEBUG);
73     // mock component name for testing if system property is set.
74     private static final String PROPERTY_EMULATED_OEM_CAR_SERVICE =
75             "persist.com.android.car.internal.debug.oem_car_service";
76 
77     private final int mOemServiceConnectionTimeoutMs;
78     private final int mOemServiceReadyTimeoutMs;
79     private final Object mLock = new Object();
80     private final boolean mIsFeatureEnabled;
81     private final Context mContext;
82     private final boolean mIsOemServiceBound;
83     private final CarOemProxyServiceHelper mHelper;
84     private final HandlerThread mHandlerThread;
85     private final Handler mHandler;
86     @GuardedBy("mLock")
87     private final ArrayList<CarOemProxyServiceCallback> mCallbacks = new ArrayList<>();
88 
89 
90     private String mComponentName;
91 
92     // True once OemService return true for {@code isOemServiceReady} call. It means that OEM
93     // service has completed all the initialization and ready to serve requests.
94     @GuardedBy("mLock")
95     private boolean mIsOemServiceReady;
96     // True once OEM service is connected. It means that OEM service has return binder for
97     // communication. OEM service may still not be ready.
98     @GuardedBy("mLock")
99     private boolean mIsOemServiceConnected;
100 
101     @GuardedBy("mLock")
102     private boolean mInitComplete;
103     @GuardedBy("mLock")
104     private IOemCarService mOemCarService;
105     @GuardedBy("mLock")
106     private CarOemAudioFocusProxyService mCarOemAudioFocusProxyService;
107     @GuardedBy("mLock")
108     private CarOemAudioVolumeProxyService mCarOemAudioVolumeProxyService;
109     @GuardedBy("mLock")
110     private CarOemAudioDuckingProxyService mCarOemAudioDuckingProxyService;
111 
112 
113     private final ServiceConnection mCarOemServiceConnection = new ServiceConnection() {
114 
115         @Override
116         public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
117             Slogf.i(TAG, "onServiceConnected: %s, %s", componentName, iBinder);
118             synchronized (mLock) {
119                 if (mOemCarService == IOemCarService.Stub.asInterface(iBinder)) {
120                     return; // already connected.
121                 }
122                 Slogf.i(TAG, "car oem service binder changed, was %s now: %s",
123                         mOemCarService, iBinder);
124                 mOemCarService = IOemCarService.Stub.asInterface(iBinder);
125                 Slogf.i(TAG, "**CarOemService connected**");
126                 mIsOemServiceConnected = true;
127                 mLock.notifyAll();
128             }
129         }
130 
131         @Override
132         public void onServiceDisconnected(ComponentName componentName) {
133             Slogf.e(TAG, "OEM service crashed. Crashing the CarService. ComponentName:%s",
134                     componentName);
135             mHelper.crashCarService("Service Disconnected");
136         }
137     };
138 
139     private final CountDownLatch mOemServiceReadyLatch = new CountDownLatch(1);
140 
141     private final IOemCarServiceCallback mOemCarServiceCallback = new IOemCarServiceCallbackImpl();
142 
143     @VisibleForTesting
CarOemProxyService(Context context)144     public CarOemProxyService(Context context) {
145         this(context, null);
146     }
147 
148     @VisibleForTesting
CarOemProxyService(Context context, CarOemProxyServiceHelper helper)149     public CarOemProxyService(Context context, CarOemProxyServiceHelper helper) {
150         this(context, helper, null);
151     }
152 
CarOemProxyService(Context context, CarOemProxyServiceHelper helper, Handler handler)153     public CarOemProxyService(Context context, CarOemProxyServiceHelper helper, Handler handler) {
154         // Bind to the OemCarService
155         mContext = context;
156         Resources res = mContext.getResources();
157         mOemServiceConnectionTimeoutMs = res
158                 .getInteger(R.integer.config_oemCarService_connection_timeout_ms);
159         mOemServiceReadyTimeoutMs = res
160                 .getInteger(R.integer.config_oemCarService_serviceReady_timeout_ms);
161 
162         String componentName = res.getString(R.string.config_oemCarService);
163 
164         if (TextUtils.isEmpty(componentName)) {
165             // mock component name for testing if system property is set.
166             String emulatedOemCarService = SystemProperties.get(PROPERTY_EMULATED_OEM_CAR_SERVICE,
167                     "");
168             if (!BuildHelper.isUserBuild() && emulatedOemCarService != null
169                     && !emulatedOemCarService.isEmpty()) {
170                 componentName = emulatedOemCarService;
171                 Slogf.i(TAG, "Using emulated componentname for testing. ComponentName: %s",
172                         mComponentName);
173             }
174         }
175 
176         mComponentName = componentName;
177 
178         Slogf.i(TAG, "Oem Car Service Config. Connection timeout:%s, Service Ready timeout:%d, "
179                 + "component Name:%s", mOemServiceConnectionTimeoutMs, mOemServiceReadyTimeoutMs,
180                 mComponentName);
181 
182         if (isInvalidComponentName(context, mComponentName)) {
183             // feature disabled
184             mIsFeatureEnabled = false;
185             mIsOemServiceBound = false;
186             mHelper = null;
187             mHandlerThread = null;
188             mHandler = null;
189             Slogf.i(TAG, "**CarOemService is disabled.**");
190             return;
191         }
192 
193         Intent intent = (new Intent())
194                 .setComponent(ComponentName.unflattenFromString(mComponentName));
195 
196         Slogf.i(TAG, "Binding to Oem Service with intent: %s", intent);
197         mHandlerThread = CarServiceUtils.getHandlerThread("car_oem_service");
198         mHandler = handler == null ? new Handler(mHandlerThread.getLooper()) : handler;
199 
200         mIsOemServiceBound = mContext.bindServiceAsUser(intent, mCarOemServiceConnection,
201                 Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT, UserHandle.SYSTEM);
202 
203         if (mIsOemServiceBound) {
204             mIsFeatureEnabled = true;
205             Slogf.i(TAG, "OemCarService bounded.");
206         } else {
207             mIsFeatureEnabled = false;
208             Slogf.e(TAG,
209                     "Couldn't bound to OemCarService. Oem service feature is marked disabled.");
210         }
211         mHelper = helper ==  null ? new CarOemProxyServiceHelper(mContext) : helper;
212     }
213 
isInvalidComponentName(Context context, String componentName)214     private boolean isInvalidComponentName(Context context, String componentName) {
215         if (componentName == null || componentName.isEmpty()) {
216             if (DBG) {
217                 Slogf.d(TAG, "ComponentName is null or empty.");
218             }
219             return true;
220         }
221 
222         // Only pre-installed package can be used for OEM Service.
223         String packageName = ComponentName.unflattenFromString(componentName).getPackageName();
224         PackageInfo info;
225         try {
226             info = context.getPackageManager().getPackageInfo(packageName, /* flags= */ 0);
227         } catch (NameNotFoundException e) {
228             Slogf.e(TAG, "componentName %s not found.", componentName);
229             return true;
230         }
231 
232         if (info == null || info.applicationInfo == null
233                 || !(PackageManagerHelper.isSystemApp(info.applicationInfo)
234                         || PackageManagerHelper.isUpdatedSystemApp(info.applicationInfo)
235                         || PackageManagerHelper.isOemApp(info.applicationInfo)
236                         || PackageManagerHelper.isOdmApp(info.applicationInfo)
237                         || PackageManagerHelper.isVendorApp(info.applicationInfo)
238                         || PackageManagerHelper.isProductApp(info.applicationInfo)
239                         || PackageManagerHelper.isSystemExtApp(info.applicationInfo))) {
240             if (DBG) {
241                 Slogf.d(TAG, "Invalid component name. Info: %s", info);
242             }
243             return true;
244         }
245 
246         if (DBG) {
247             Slogf.d(TAG, "Valid component name %s, ", componentName);
248         }
249 
250         return false;
251     }
252 
253     /**
254      * Registers callback to be called once OEM service is ready.
255      *
256      * <p>Other CarService components cannot call OEM service. But they can register a callback
257      * which would be called as soon as OEM Service is ready./
258      */
registerCallback(CarOemProxyServiceCallback callback)259     public void registerCallback(CarOemProxyServiceCallback callback) {
260         synchronized (mLock) {
261             mCallbacks.add(callback);
262         }
263     }
264 
265     /**
266      * Informs if OEM service is enabled.
267      */
isOemServiceEnabled()268     public boolean isOemServiceEnabled() {
269         synchronized (mLock) {
270             return mIsFeatureEnabled;
271         }
272     }
273 
274     /**
275      * Informs if OEM service is ready.
276      */
isOemServiceReady()277     public boolean isOemServiceReady() {
278         synchronized (mLock) {
279             return mIsOemServiceReady;
280         }
281     }
282 
283     @Override
init()284     public void init() {
285         // Nothing to be done as OemCarService was initialized in the constructor.
286     }
287 
288     @Override
release()289     public void release() {
290         // Stop OEM Service;
291         if (mIsOemServiceBound) {
292             Slogf.i(TAG, "Unbinding Oem Service");
293             mContext.unbindService(mCarOemServiceConnection);
294         }
295     }
296 
297     @Override
dump(IndentingPrintWriter writer)298     public void dump(IndentingPrintWriter writer) {
299         writer.println("***CarOemProxyService dump***");
300         writer.increaseIndent();
301         synchronized (mLock) {
302             writer.printf("mIsFeatureEnabled: %s\n", mIsFeatureEnabled);
303             writer.printf("mIsOemServiceBound: %s\n", mIsOemServiceBound);
304             writer.printf("mIsOemServiceReady: %s\n", mIsOemServiceReady);
305             writer.printf("mIsOemServiceConnected: %s\n", mIsOemServiceConnected);
306             writer.printf("mInitComplete: %s\n", mInitComplete);
307             writer.printf("OEM_CAR_SERVICE_CONNECTED_TIMEOUT_MS: %s\n",
308                     mOemServiceConnectionTimeoutMs);
309             writer.printf("OEM_CAR_SERVICE_READY_TIMEOUT_MS: %s\n", mOemServiceReadyTimeoutMs);
310             writer.printf("mComponentName: %s\n", mComponentName);
311             writer.printf("mCallbacks size: %d\n", mCallbacks.size());
312             // Dump OEM service stack
313             if (mIsOemServiceReady) {
314                 writer.printf("OEM callstack\n");
315                 int timeoutMs = 2000;
316                 try {
317                     IOemCarService oemCarService = getOemService();
318                     writer.printf(mHelper.doBinderTimedCallWithTimeout(CALL_TAG,
319                             () -> oemCarService.getAllStackTraces(), timeoutMs));
320                 } catch (TimeoutException e) {
321                     writer.printf("Didn't received OEM stack within %d milliseconds.\n", timeoutMs);
322                 }
323             }
324             // Dump helper
325             if (mHelper != null) {
326                 mHelper.dump(writer);
327             }
328         }
329         writer.decreaseIndent();
330     }
331 
getOemServiceName()332     public String getOemServiceName() {
333         return mComponentName;
334     }
335 
336     /**
337      * Gets OEM audio focus service.
338      */
339     @Nullable
getCarOemAudioFocusService()340     public CarOemAudioFocusProxyService getCarOemAudioFocusService() {
341         if (!mIsFeatureEnabled) {
342             if (DBG) {
343                 Slogf.d(TAG, "Oem Car Service is disabled, returning null for"
344                         + " getCarOemAudioFocusService");
345             }
346             return null;
347         }
348 
349         synchronized (mLock) {
350             if (mCarOemAudioFocusProxyService != null) {
351                 return mCarOemAudioFocusProxyService;
352             }
353         }
354 
355         waitForOemService();
356 
357         // Defaults to returning null service and try again next time the service is requested.
358         IOemCarService oemCarService = getOemService();
359         IOemCarAudioFocusService oemAudioFocusService = mHelper.doBinderTimedCallWithDefaultValue(
360                 CALL_TAG, () -> oemCarService.getOemAudioFocusService(),
361                 /* defaultValue= */ null);
362 
363         if (oemAudioFocusService == null) {
364             if (DBG) {
365                 Slogf.d(TAG, "Oem Car Service doesn't implement AudioFocusService, returning null"
366                         + " for getCarOemAudioFocusService");
367             }
368             return null;
369         }
370 
371         CarOemAudioFocusProxyService carOemAudioFocusProxyService =
372                 new CarOemAudioFocusProxyService(mHelper, oemAudioFocusService);
373         synchronized (mLock) {
374             if (mCarOemAudioFocusProxyService != null) {
375                 return mCarOemAudioFocusProxyService;
376             }
377             mCarOemAudioFocusProxyService = carOemAudioFocusProxyService;
378             Slogf.i(TAG, "CarOemAudioFocusProxyService is ready.");
379             return mCarOemAudioFocusProxyService;
380         }
381     }
382 
383     /**
384      * Gets OEM audio volume service.
385      */
386     @Nullable
getCarOemAudioVolumeService()387     public CarOemAudioVolumeProxyService getCarOemAudioVolumeService() {
388         if (!mIsFeatureEnabled) {
389             if (DBG) {
390                 Slogf.d(TAG, "Oem Car Service is disabled, returning null for"
391                         + " getCarOemAudioVolumeService");
392             }
393             return null;
394         }
395 
396         synchronized (mLock) {
397             if (mCarOemAudioVolumeProxyService != null) {
398                 return mCarOemAudioVolumeProxyService;
399             }
400         }
401 
402         waitForOemService();
403         IOemCarService oemCarService = getOemService();
404         IOemCarAudioVolumeService oemAudioVolumeService = mHelper.doBinderTimedCallWithDefaultValue(
405                 CALL_TAG, () -> oemCarService.getOemAudioVolumeService(),
406                 /* defaultValue= */ null);
407 
408         if (oemAudioVolumeService == null) {
409             if (DBG) {
410                 Slogf.d(TAG, "Oem Car Service doesn't implement AudioVolumeService,"
411                         + "returning null for getCarOemAudioDuckingService");
412             }
413             return null;
414         }
415 
416         CarOemAudioVolumeProxyService carOemAudioVolumeProxyService =
417                 new CarOemAudioVolumeProxyService(mHelper, oemAudioVolumeService);
418         synchronized (mLock) {
419             if (mCarOemAudioVolumeProxyService != null) {
420                 return mCarOemAudioVolumeProxyService;
421             }
422             mCarOemAudioVolumeProxyService = carOemAudioVolumeProxyService;
423             Slogf.i(TAG, "CarOemAudioVolumeProxyService is ready.");
424         }
425         return carOemAudioVolumeProxyService;
426     }
427 
428     /**
429      * Gets OEM audio ducking service.
430      */
431     @Nullable
getCarOemAudioDuckingService()432     public CarOemAudioDuckingProxyService getCarOemAudioDuckingService() {
433         if (!mIsFeatureEnabled) {
434             if (DBG) {
435                 Slogf.d(TAG, "Oem Car Service is disabled, returning null for"
436                         + " getCarOemAudioDuckingService");
437             }
438             return null;
439         }
440 
441         synchronized (mLock) {
442             if (mCarOemAudioDuckingProxyService != null) {
443                 return mCarOemAudioDuckingProxyService;
444             }
445         }
446 
447         waitForOemService();
448 
449         IOemCarService oemCarService = getOemService();
450         IOemCarAudioDuckingService oemAudioDuckingService =
451                 mHelper.doBinderTimedCallWithDefaultValue(
452                 CALL_TAG, () -> oemCarService.getOemAudioDuckingService(),
453                 /* defaultValue= */ null);
454 
455         if (oemAudioDuckingService == null) {
456             if (DBG) {
457                 Slogf.d(TAG, "Oem Car Service doesn't implement AudioDuckingService,"
458                         + "returning null for getCarOemAudioDuckingService");
459             }
460             return null;
461         }
462 
463         CarOemAudioDuckingProxyService carOemAudioDuckingProxyService =
464                 new CarOemAudioDuckingProxyService(mHelper, oemAudioDuckingService);
465         synchronized (mLock) {
466             if (mCarOemAudioDuckingProxyService != null) {
467                 return mCarOemAudioDuckingProxyService;
468             }
469             mCarOemAudioDuckingProxyService = carOemAudioDuckingProxyService;
470             Slogf.i(TAG, "CarOemAudioDuckingProxyService is ready.");
471         }
472         return carOemAudioDuckingProxyService;
473     }
474 
475     /**
476      * Should be called when CarService is ready for communication. It updates the OEM service that
477      * CarService is ready.
478      */
onCarServiceReady()479     public void onCarServiceReady() {
480         waitForOemServiceConnected();
481         IOemCarService oemCarService = getOemService();
482         mHelper.doBinderOneWayCall(CALL_TAG, () -> {
483             try {
484                 oemCarService.onCarServiceReady(mOemCarServiceCallback);
485             } catch (RemoteException ex) {
486                 Slogf.e(TAG, "Binder call received RemoteException, calling to crash CarService",
487                         ex);
488             }
489         });
490         waitForOemServiceReady();
491     }
492 
waitForOemServiceConnected()493     private void waitForOemServiceConnected() {
494         synchronized (mLock) {
495             if (!mInitComplete) {
496                 // No CarOemService call should be made before or during init of ICarImpl.
497                 throw new IllegalStateException(
498                         "CarOemService should not be call before CarService initialization");
499             }
500 
501             if (mIsOemServiceConnected) {
502                 return;
503             }
504             waitForOemServiceConnectedLocked();
505         }
506     }
507 
508     @GuardedBy("mLock")
waitForOemServiceConnectedLocked()509     private void waitForOemServiceConnectedLocked() {
510         long startTime = SystemClock.elapsedRealtime();
511         long remainingTime = mOemServiceConnectionTimeoutMs;
512 
513         while (!mIsOemServiceConnected && remainingTime > 0) {
514             try {
515                 Slogf.i(TAG, "waiting to connect to OemService. wait time: %s", remainingTime);
516                 mLock.wait(mOemServiceConnectionTimeoutMs);
517                 remainingTime = mOemServiceConnectionTimeoutMs
518                         - (SystemClock.elapsedRealtime() - startTime);
519             } catch (InterruptedException e) {
520                 Thread.currentThread().interrupt();
521                 Slogf.w(TAG, "InterruptedException received. Reset interrupted status.", e);
522             }
523         }
524 
525         if (!mIsOemServiceConnected) {
526             Slogf.e(TAG, "OEM Service is not connected within: %dms, calling to crash CarService",
527                     mOemServiceConnectionTimeoutMs);
528             mHelper.crashCarService("OEM Service not connected");
529         }
530     }
531 
waitForOemService()532     private void waitForOemService() {
533         waitForOemServiceConnected();
534         waitForOemServiceReady();
535     }
536 
waitForOemServiceReady()537     private void waitForOemServiceReady() {
538         synchronized (mLock) {
539             if (mIsOemServiceReady) {
540                 return;
541             }
542         }
543 
544         try {
545             mOemServiceReadyLatch.await(mOemServiceReadyTimeoutMs, TimeUnit.MILLISECONDS);
546         } catch (InterruptedException e) {
547             Thread.currentThread().interrupt();
548             Slogf.i(TAG, "Exception while waiting for OEM Service to be ready.", e);
549         }
550 
551         synchronized (mLock) {
552             if (!mIsOemServiceReady) {
553                 Slogf.e(TAG, "OEM Service is not ready within: " + mOemServiceReadyTimeoutMs
554                         + "ms, calling to crash CarService");
555                 mHelper.crashCarService("OEM Service not ready");
556             }
557         }
558         Slogf.i(TAG, "OEM Service is ready.");
559     }
560 
561     // Initialize all OEM related components.
initOemServiceComponents()562     private void initOemServiceComponents() {
563         // Initialize all Oem Service components
564         getCarOemAudioFocusService();
565 
566         // Callback registered Car Service components for OEM service.
567         callCarServiceComponents();
568     }
569 
callCarServiceComponents()570     private void callCarServiceComponents() {
571         synchronized (mLock) {
572             for (int i = 0; i < mCallbacks.size(); i++) {
573                 mCallbacks.get(i).onOemServiceReady();
574             }
575         }
576     }
577 
578     /**
579      * Informs CarOemService that ICarImpl's init is complete.
580      */
581     // This would set mInitComplete, which is an additional check so that no car service component
582     // calls CarOemService during or before ICarImpl's init.
onInitComplete()583     public void onInitComplete() {
584         if (!mIsFeatureEnabled) {
585             if (DBG) {
586                 Slogf.d(TAG, "Oem Car Service is disabled, No-op for onInitComplete");
587             }
588             return;
589         }
590 
591         synchronized (mLock) {
592             mInitComplete = true;
593         }
594         // inform OEM Service that CarService is ready for communication.
595         // It has to be posted on the different thread as this call is part of init process.
596         mHandler.post(() -> onCarServiceReady());
597     }
598 
599     /**
600      * Gets OEM service latest binder. Don't pass the method to helper as it can cause deadlock.
601      */
getOemService()602     private IOemCarService getOemService() {
603         synchronized (mLock) {
604             return mOemCarService;
605         }
606     }
607 
608     private class IOemCarServiceCallbackImpl extends IOemCarServiceCallback.Stub {
609         @Override
sendOemCarServiceReady()610         public void sendOemCarServiceReady() {
611             synchronized (mLock) {
612                 mIsOemServiceReady = true;
613             }
614             mOemServiceReadyLatch.countDown();
615             int pid = Binder.getCallingPid();
616             Slogf.i(TAG, "OEM Car service is ready and running. Process ID of OEM Car Service is:"
617                     + " %d", pid);
618             mHelper.updateOemPid(pid);
619             IOemCarService oemCarService = getOemService();
620             mHelper.updateOemStackCall(() -> oemCarService.getAllStackTraces());
621             // Initialize other components on handler thread so that main thread is not
622             // blocked
623             mHandler.post(() -> initOemServiceComponents());
624         }
625     }
626 }
627