1 /*
2  * Copyright (C) 2017 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 package android.car.cluster;
17 
18 import static android.content.Intent.ACTION_USER_UNLOCKED;
19 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
20 import static android.view.Display.INVALID_DISPLAY;
21 
22 import static java.lang.Integer.parseInt;
23 
24 import android.annotation.Nullable;
25 import android.app.ActivityManager;
26 import android.app.ActivityOptions;
27 import android.car.Car;
28 import android.car.CarAppFocusManager;
29 import android.car.cluster.navigation.NavigationState.NavigationStateProto;
30 import android.car.cluster.renderer.InstrumentClusterRenderingService;
31 import android.car.cluster.renderer.NavigationRenderer;
32 import android.car.navigation.CarNavigationInstrumentCluster;
33 import android.content.BroadcastReceiver;
34 import android.content.ComponentName;
35 import android.content.Context;
36 import android.content.ContextWrapper;
37 import android.content.Intent;
38 import android.content.IntentFilter;
39 import android.content.pm.ActivityInfo;
40 import android.content.pm.PackageManager;
41 import android.graphics.Rect;
42 import android.hardware.display.DisplayManager;
43 import android.hardware.display.DisplayManager.DisplayListener;
44 import android.os.Binder;
45 import android.os.Bundle;
46 import android.os.Handler;
47 import android.os.IBinder;
48 import android.os.SystemClock;
49 import android.os.UserHandle;
50 import android.provider.Settings;
51 import android.provider.Settings.Global;
52 import android.util.Log;
53 import android.view.Display;
54 import android.view.InputDevice;
55 import android.view.KeyEvent;
56 
57 import com.android.car.internal.common.UserHelperLite;
58 
59 import com.google.protobuf.InvalidProtocolBufferException;
60 
61 import java.io.FileDescriptor;
62 import java.io.PrintWriter;
63 import java.util.ArrayList;
64 import java.util.Arrays;
65 import java.util.List;
66 import java.util.Objects;
67 import java.util.function.Consumer;
68 
69 /**
70  * Implementation of {@link InstrumentClusterRenderingService} which renders an activity on a
71  * virtual display that is transmitted to an external screen.
72  */
73 public class ClusterRenderingService extends InstrumentClusterRenderingService implements
74         ImageResolver.BitmapFetcher, CarAppFocusManager.OnAppFocusChangedListener {
75     private static final String TAG = "Cluster.Service";
76 
77     static final String LOCAL_BINDING_ACTION = "local";
78     static final String NAV_STATE_PROTO_BUNDLE_KEY = "navstate2";
79 
80     private List<ServiceClient> mClients = new ArrayList<>();
81     private ClusterDisplayProvider mDisplayProvider;
82 
83     private int mClusterDisplayId = INVALID_DISPLAY;
84 
85     private boolean mInstrumentClusterHelperReady;
86 
87     private final IBinder mLocalBinder = new LocalBinder();
88     private final ImageResolver mImageResolver = new ImageResolver(this);
89     private final Handler mHandler = new Handler();
90     private final Runnable mLaunchMainActivity = this::launchMainActivity;
91     private ComponentName mNavigationClusterActivity = null;
92     private int mNavigationClusterUserId = UserHandle.USER_SYSTEM;
93     private CarAppFocusManager mAppFocusManager = null;
94 
95     private final UserReceiver mUserReceiver = new UserReceiver();
96 
97     public interface ServiceClient {
onKeyEvent(KeyEvent keyEvent)98         void onKeyEvent(KeyEvent keyEvent);
99 
onNavigationStateChange(NavigationStateProto navState)100         void onNavigationStateChange(NavigationStateProto navState);
101     }
102 
103     public class LocalBinder extends Binder {
getService()104         ClusterRenderingService getService() {
105             return ClusterRenderingService.this;
106         }
107     }
108 
109     private final DisplayListener mDisplayListener = new DisplayListener() {
110         // Called in the main thread, since ClusterDisplayProvider.DisplayListener was registered
111         // with null handler.
112         @Override
113         public void onDisplayAdded(int displayId) {
114             Log.i(TAG, "Cluster display found, displayId: " + displayId);
115             mClusterDisplayId = displayId;
116             if (mInstrumentClusterHelperReady) {
117                 mHandler.post(mLaunchMainActivity);
118             }
119         }
120 
121         @Override
122         public void onDisplayRemoved(int displayId) {
123             Log.w(TAG, "Cluster display has been removed");
124         }
125 
126         @Override
127         public void onDisplayChanged(int displayId) {
128 
129         }
130     };
131 
setActivityLaunchOptions(int displayId, ClusterActivityState state)132     public void setActivityLaunchOptions(int displayId, ClusterActivityState state) {
133         ActivityOptions options = displayId != INVALID_DISPLAY
134                 ? ActivityOptions.makeBasic().setLaunchDisplayId(displayId)
135                 : null;
136         setClusterActivityLaunchOptions(options);
137         if (Log.isLoggable(TAG, Log.DEBUG)) {
138             Log.d(TAG, String.format("activity options set: %s (displayeId: %d)",
139                     options, options != null ? options.getLaunchDisplayId() : -1));
140         }
141         setClusterActivityState(state);
142         if (Log.isLoggable(TAG, Log.DEBUG)) {
143             Log.d(TAG, String.format("activity state set: %s", state));
144         }
145     }
146 
registerClient(ServiceClient client)147     public void registerClient(ServiceClient client) {
148         mClients.add(client);
149     }
150 
unregisterClient(ServiceClient client)151     public void unregisterClient(ServiceClient client) {
152         mClients.remove(client);
153     }
154 
getImageResolver()155     public ImageResolver getImageResolver() {
156         return mImageResolver;
157     }
158 
159     @Override
onBind(Intent intent)160     public IBinder onBind(Intent intent) {
161         Log.d(TAG, "onBind, intent: " + intent);
162         if (LOCAL_BINDING_ACTION.equals(intent.getAction())) {
163             return mLocalBinder;
164         }
165         IBinder binder = super.onBind(intent);
166         mInstrumentClusterHelperReady = true;
167         if (mClusterDisplayId != INVALID_DISPLAY) {
168             mHandler.post(mLaunchMainActivity);
169         }
170         return binder;
171     }
172 
173     @Override
onCreate()174     public void onCreate() {
175         super.onCreate();
176         Log.d(TAG, "onCreate");
177         // The following will never be null, as this service is initiated by CarService itself.
178         Car car = Car.createCar(this);
179         mAppFocusManager = (CarAppFocusManager) car.getCarManager(Car.APP_FOCUS_SERVICE);
180         mAppFocusManager.addFocusListener(this, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
181         mDisplayProvider = new ClusterDisplayProvider(this, mDisplayListener);
182         mUserReceiver.register(this);
183         mNavigationClusterActivity = getNavigationClusterActivity();
184         Log.i(TAG, "onCreate: set cluster to " + mNavigationClusterActivity);
185     }
186 
187     @Override
onDestroy()188     public void onDestroy() {
189         super.onDestroy();
190         mAppFocusManager.removeFocusListener(this, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
191         mUserReceiver.unregister(this);
192         mDisplayProvider.release();
193     }
194 
195     @Override
onAppFocusChanged(int appType, boolean active)196     public void onAppFocusChanged(int appType, boolean active) {
197         boolean useNavigationOnly = getResources().getBoolean(R.bool.navigationOnly);
198         Log.i(TAG, "onAppFocusChanged: " + appType + ", active: " + active);
199         if (useNavigationOnly) {
200             launchMainActivity();
201         } else {
202             // TODO(b/193931272): Update MainClusterActivity
203         }
204     }
205 
launchMainActivity()206     private void launchMainActivity() {
207         mHandler.removeCallbacks(mLaunchMainActivity);
208         ActivityOptions options = ActivityOptions.makeBasic();
209         options.setLaunchDisplayId(mClusterDisplayId);
210         boolean useNavigationOnly = getResources().getBoolean(R.bool.navigationOnly);
211         Intent intent;
212         int userId = UserHandle.USER_SYSTEM;
213         if (useNavigationOnly) {
214             userId = ActivityManager.getCurrentUser();
215             if (UserHelperLite.isHeadlessSystemUser(userId)) {
216                 Log.i(TAG, "Skipping the navigation activity for User 0");
217                 return;
218             }
219             ComponentName newClusterActivity = getNavigationClusterActivity();
220             if (Objects.equals(newClusterActivity, mNavigationClusterActivity)
221                     && userId == mNavigationClusterUserId) {
222                 Log.i(TAG, "Cluster activity hasn't changed. Skipping.");
223                 return;
224             }
225             Log.i(TAG, "Set cluster to " + newClusterActivity);
226             onNavigationComponentChanged(newClusterActivity);
227             mNavigationClusterActivity = newClusterActivity;
228             mNavigationClusterUserId = userId;
229             intent = getNavigationActivityIntent(mNavigationClusterActivity, mClusterDisplayId);
230             startFixedActivityModeForDisplayAndUser(intent, options, userId);
231         } else {
232             intent = getMainClusterActivityIntent();
233             startActivityAsUser(intent, options.toBundle(), UserHandle.SYSTEM);
234         }
235         Log.i(TAG, "launching main activity=" + intent + ", display=" + mClusterDisplayId
236                 + ", userId=" + userId);
237     }
238 
239     /**
240      * Invoked when the activity to show in the cluster changes
241      *
242      * @param clusterActivity current activity displayed in cluster. If no application is holding
243      *                        {@link CarAppFocusManager#APP_FOCUS_TYPE_NAVIGATION}, this will be the
244      *                        default map cluster activity. Otherwise, this will be the cluster
245      *                        activity of the focused application (if it has one) or {@code null} if
246      *                        the application doesn't have a cluster activity or the activity is
247      *                        disabled.
248      */
onNavigationComponentChanged(@ullable ComponentName clusterActivity)249     protected void onNavigationComponentChanged(@Nullable ComponentName clusterActivity) {
250         // This method can be used by OEMs to send a signal to the cluster hardware indicating
251         // whether Android has or doesn't have a cluster activity.
252         //
253         // OEMs can use this signal to let the cluster show some other view, or to hide Android's
254         // video feed altogether.
255     }
256 
getMainClusterActivityIntent()257     private Intent getMainClusterActivityIntent() {
258         return new Intent(this, MainClusterActivity.class).setFlags(FLAG_ACTIVITY_NEW_TASK);
259     }
260 
getNavigationClusterActivity()261     private ComponentName getNavigationClusterActivity() {
262         List<String> focusOwnerPackageNames = mAppFocusManager.getAppTypeOwner(
263                 CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
264 
265         if (focusOwnerPackageNames == null || focusOwnerPackageNames.isEmpty()) {
266             // No application has focus. We use the default navigation app.
267             Log.i(TAG, "getNavigationClusterActivity(): no focus owner -> "
268                     + "using default nav app");
269             ActivityInfo activityInfo = MainClusterActivity.getNavigationActivity(this);
270             return new ComponentName(activityInfo.packageName, activityInfo.name);
271         } else {
272             ComponentName clusterActivity = getComponentFromPackages(focusOwnerPackageNames);
273             if (clusterActivity == null) {
274                 // If currently focused app has no cluster activity, we indicate so.
275                 Log.i(TAG, "getNavigationClusterActivity(): focus owned by "
276                         + focusOwnerPackageNames + " but it has no cluster activity -> "
277                         + "using empty activity");
278                 return null;
279             }
280             // Otherwise, we use the activity of the currently focused app
281             Log.i(TAG, "getNavigationClusterActivity(): focus owned and it has a cluster "
282                     + "activity -> using " + focusOwnerPackageNames + " app");
283             return clusterActivity;
284         }
285     }
286 
getComponentFromPackages(List<String> packageNames)287     private ComponentName getComponentFromPackages(List<String> packageNames) {
288         for (String packageName : packageNames) {
289             ComponentName result = getComponentFromPackage(packageName);
290             if (result != null) {
291                 return result;
292             }
293         }
294         return null;
295     }
296 
getNavigationActivityIntent(ComponentName component, int displayId)297     private Intent getNavigationActivityIntent(ComponentName component, int displayId) {
298         if (component == null) {
299             Log.i(TAG, "Focused application doesn't have a cluster activity. Using fallback.");
300             component = new ComponentName(this, EmptyNavigationActivity.class);
301         }
302         Rect displaySize = new Rect(0, 0, 240, 320);  // Arbitrary size, better than nothing.
303         DisplayManager dm = getSystemService(DisplayManager.class);
304         Display display = dm.getDisplay(displayId);
305         if (display != null) {
306             display.getRectSize(displaySize);
307         }
308         setClusterActivityState(ClusterActivityState.create(/* visible= */ true,
309                     /* unobscuredBounds= */ new Rect(0, 0, 240, 320)));
310         return new Intent(Intent.ACTION_MAIN)
311             .setComponent(component)
312             .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
313             .putExtra(Car.CAR_EXTRA_CLUSTER_ACTIVITY_STATE,
314                 ClusterActivityState.create(/* visible= */ true,
315                     /* unobscuredBounds= */ displaySize).toBundle());
316     }
317 
318     @Override
onKeyEvent(KeyEvent keyEvent)319     public void onKeyEvent(KeyEvent keyEvent) {
320         if (Log.isLoggable(TAG, Log.DEBUG)) {
321             Log.d(TAG, "onKeyEvent, keyEvent: " + keyEvent);
322         }
323         broadcastClientEvent(client -> client.onKeyEvent(keyEvent));
324     }
325 
326     /**
327      * Broadcasts an event to all the registered service clients
328      *
329      * @param event event to broadcast
330      */
broadcastClientEvent(Consumer<ServiceClient> event)331     private void broadcastClientEvent(Consumer<ServiceClient> event) {
332         for (ServiceClient client : mClients) {
333             event.accept(client);
334         }
335     }
336 
337     @Override
getNavigationRenderer()338     public NavigationRenderer getNavigationRenderer() {
339         NavigationRenderer navigationRenderer = new NavigationRenderer() {
340             @Override
341             public CarNavigationInstrumentCluster getNavigationProperties() {
342                 CarNavigationInstrumentCluster config =
343                         CarNavigationInstrumentCluster.createCluster(1000);
344                 Log.d(TAG, "getNavigationProperties, returns: " + config);
345                 return config;
346             }
347 
348             @Override
349             public void onNavigationStateChanged(Bundle bundle) {
350                 StringBuilder bundleSummary = new StringBuilder();
351 
352                 // Attempt to read proto byte array
353                 byte[] protoBytes = bundle.getByteArray(NAV_STATE_PROTO_BUNDLE_KEY);
354                 if (protoBytes != null) {
355                     try {
356                         NavigationStateProto navState = NavigationStateProto.parseFrom(
357                                 protoBytes);
358                         bundleSummary.append(navState.toString());
359 
360                         // Update clients
361                         broadcastClientEvent(
362                                 client -> client.onNavigationStateChange(navState));
363                     } catch (InvalidProtocolBufferException e) {
364                         Log.e(TAG, "Error parsing navigation state proto", e);
365                     }
366                 } else {
367                     Log.e(TAG, "Received nav state byte array is null");
368                 }
369                 Log.d(TAG, "onNavigationStateChanged(" + bundleSummary + ")");
370             }
371         };
372 
373         Log.i(TAG, "createNavigationRenderer, returns: " + navigationRenderer);
374         return navigationRenderer;
375     }
376 
377     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)378     protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
379         if (args != null && args.length > 0) {
380             execShellCommand(args);
381         } else {
382             super.dump(fd, writer, args);
383             writer.println("DisplayProvider: " + mDisplayProvider);
384         }
385     }
386 
emulateKeyEvent(int keyCode)387     private void emulateKeyEvent(int keyCode) {
388         Log.i(TAG, "emulateKeyEvent, keyCode: " + keyCode);
389         long downTime = SystemClock.uptimeMillis();
390         long eventTime = SystemClock.uptimeMillis();
391         KeyEvent event = obtainKeyEvent(keyCode, downTime, eventTime, KeyEvent.ACTION_DOWN);
392         onKeyEvent(event);
393 
394         eventTime = SystemClock.uptimeMillis();
395         event = obtainKeyEvent(keyCode, downTime, eventTime, KeyEvent.ACTION_UP);
396         onKeyEvent(event);
397     }
398 
obtainKeyEvent(int keyCode, long downTime, long eventTime, int action)399     private KeyEvent obtainKeyEvent(int keyCode, long downTime, long eventTime, int action) {
400         int scanCode = 0;
401         if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
402             scanCode = 108;
403         } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
404             scanCode = 106;
405         }
406         return KeyEvent.obtain(
407                 downTime,
408                 eventTime,
409                 action,
410                 keyCode,
411                 0 /* repeat */,
412                 0 /* meta state */,
413                 0 /* deviceId*/,
414                 scanCode /* scancode */,
415                 KeyEvent.FLAG_FROM_SYSTEM /* flags */,
416                 InputDevice.SOURCE_KEYBOARD,
417                 null /* characters */);
418     }
419 
execShellCommand(String[] args)420     private void execShellCommand(String[] args) {
421         Log.i(TAG, "execShellCommand, args: " + Arrays.toString(args));
422 
423         String command = args[0];
424 
425         switch (command) {
426             case "injectKey": {
427                 if (args.length > 1) {
428                     emulateKeyEvent(parseInt(args[1]));
429                 } else {
430                     Log.i(TAG, "Not enough arguments");
431                 }
432                 break;
433             }
434             case "destroyOverlayDisplay": {
435                 Settings.Global.putString(getContentResolver(),
436                         Global.OVERLAY_DISPLAY_DEVICES, "");
437                 break;
438             }
439 
440             case "createOverlayDisplay": {
441                 if (args.length > 1) {
442                     Settings.Global.putString(getContentResolver(),
443                             Global.OVERLAY_DISPLAY_DEVICES, args[1]);
444                 } else {
445                     Log.i(TAG, "Not enough arguments, expected 2");
446                 }
447                 break;
448             }
449 
450             case "setUnobscuredArea": {
451                 if (args.length > 5) {
452                     setClusterActivityState(ClusterActivityState.create(true,
453                             new Rect(parseInt(args[2]), parseInt(args[3]),
454                                     parseInt(args[4]), parseInt(args[5]))));
455                 } else {
456                     Log.i(TAG, "wrong format, expected: category left top right bottom");
457                 }
458             }
459         }
460     }
461 
462     private class UserReceiver extends BroadcastReceiver {
register(Context context)463         void register(Context context) {
464             IntentFilter intentFilter = new IntentFilter(ACTION_USER_UNLOCKED);
465             context.registerReceiverAsUser(this, UserHandle.ALL, intentFilter, null, null);
466         }
467 
unregister(Context context)468         void unregister(Context context) {
469             context.unregisterReceiver(this);
470         }
471 
472         @Override
onReceive(Context context, Intent intent)473         public void onReceive(Context context, Intent intent) {
474             if (Log.isLoggable(TAG, Log.DEBUG)) {
475                 Log.d(TAG, "Broadcast received: " + intent);
476             }
477             int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
478             if (userId == ActivityManager.getCurrentUser() &&
479                 mInstrumentClusterHelperReady && mClusterDisplayId != INVALID_DISPLAY) {
480                 mHandler.post(mLaunchMainActivity);
481             }
482         }
483     }
484 }
485