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_SWITCHED;
19 import static android.content.Intent.ACTION_USER_UNLOCKED;
20 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
21 import static android.view.Display.INVALID_DISPLAY;
22 
23 import static java.lang.Integer.parseInt;
24 
25 import android.app.ActivityManager;
26 import android.app.ActivityOptions;
27 import android.car.CarNotConnectedException;
28 import android.car.cluster.navigation.NavigationState.NavigationStateProto;
29 import android.car.cluster.renderer.InstrumentClusterRenderingService;
30 import android.car.cluster.renderer.NavigationRenderer;
31 import android.car.navigation.CarNavigationInstrumentCluster;
32 import android.content.BroadcastReceiver;
33 import android.content.ComponentName;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.IntentFilter;
37 import android.graphics.Rect;
38 import android.hardware.display.DisplayManager;
39 import android.hardware.display.DisplayManager.DisplayListener;
40 import android.os.Binder;
41 import android.os.Bundle;
42 import android.os.Handler;
43 import android.os.IBinder;
44 import android.os.SystemClock;
45 import android.os.UserHandle;
46 import android.provider.Settings;
47 import android.provider.Settings.Global;
48 import android.util.Log;
49 import android.view.Display;
50 import android.view.InputDevice;
51 import android.view.KeyEvent;
52 
53 import com.google.protobuf.InvalidProtocolBufferException;
54 
55 import java.io.FileDescriptor;
56 import java.io.PrintWriter;
57 import java.util.ArrayList;
58 import java.util.Arrays;
59 import java.util.List;
60 import java.util.function.Consumer;
61 
62 /**
63  * Implementation of {@link InstrumentClusterRenderingService} which renders an activity on a
64  * virtual display that is transmitted to an external screen.
65  */
66 public class ClusterRenderingService extends InstrumentClusterRenderingService implements
67         ImageResolver.BitmapFetcher {
68     private static final String TAG = "Cluster.Service";
69     private static final int NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS = 1000;
70 
71     static final int NAV_STATE_EVENT_ID = 1;
72     static final String LOCAL_BINDING_ACTION = "local";
73     static final String NAV_STATE_PROTO_BUNDLE_KEY = "navstate2";
74 
75     private List<ServiceClient> mClients = new ArrayList<>();
76     private ClusterDisplayProvider mDisplayProvider;
77 
78     private int mClusterDisplayId = INVALID_DISPLAY;
79 
80     private boolean mInstrumentClusterHelperReady;
81 
82     private final IBinder mLocalBinder = new LocalBinder();
83     private final ImageResolver mImageResolver = new ImageResolver(this);
84     private final Handler mHandler = new Handler();
85     private final Runnable mLaunchMainActivity = this::launchMainActivity;
86 
87     private final UserReceiver mUserReceiver = new UserReceiver();
88 
89     public interface ServiceClient {
onKeyEvent(KeyEvent keyEvent)90         void onKeyEvent(KeyEvent keyEvent);
91 
onNavigationStateChange(NavigationStateProto navState)92         void onNavigationStateChange(NavigationStateProto navState);
93     }
94 
95     public class LocalBinder extends Binder {
getService()96         ClusterRenderingService getService() {
97             return ClusterRenderingService.this;
98         }
99     }
100 
101     private final DisplayListener mDisplayListener = new DisplayListener() {
102         // Called in the main thread, since ClusterDisplayProvider.DisplayListener was registered
103         // with null handler.
104         @Override
105         public void onDisplayAdded(int displayId) {
106             Log.i(TAG, "Cluster display found, displayId: " + displayId);
107             mClusterDisplayId = displayId;
108             if (mInstrumentClusterHelperReady) {
109                 mHandler.post(mLaunchMainActivity);
110             }
111         }
112 
113         @Override
114         public void onDisplayRemoved(int displayId) {
115             Log.w(TAG, "Cluster display has been removed");
116         }
117 
118         @Override
119         public void onDisplayChanged(int displayId) {
120 
121         }
122     };
123 
setActivityLaunchOptions(int displayId, ClusterActivityState state)124     public void setActivityLaunchOptions(int displayId, ClusterActivityState state) {
125         try {
126             ActivityOptions options = displayId != INVALID_DISPLAY
127                     ? ActivityOptions.makeBasic().setLaunchDisplayId(displayId)
128                     : null;
129             setClusterActivityLaunchOptions(CarInstrumentClusterManager.CATEGORY_NAVIGATION,
130                     options);
131             if (Log.isLoggable(TAG, Log.DEBUG)) {
132                 Log.d(TAG, String.format("activity options set: %s (displayeId: %d)",
133                         options, options.getLaunchDisplayId()));
134             }
135             setClusterActivityState(CarInstrumentClusterManager.CATEGORY_NAVIGATION,
136                     state.toBundle());
137             if (Log.isLoggable(TAG, Log.DEBUG)) {
138                 Log.d(TAG, String.format("activity state set: %s", state));
139             }
140         } catch (CarNotConnectedException ex) {
141             Log.e(TAG, "Unable to update service", ex);
142         }
143     }
144 
registerClient(ServiceClient client)145     public void registerClient(ServiceClient client) {
146         mClients.add(client);
147     }
148 
unregisterClient(ServiceClient client)149     public void unregisterClient(ServiceClient client) {
150         mClients.remove(client);
151     }
152 
getImageResolver()153     public ImageResolver getImageResolver() {
154         return mImageResolver;
155     }
156 
157     @Override
onBind(Intent intent)158     public IBinder onBind(Intent intent) {
159         Log.d(TAG, "onBind, intent: " + intent);
160         if (LOCAL_BINDING_ACTION.equals(intent.getAction())) {
161             return mLocalBinder;
162         }
163         IBinder binder = super.onBind(intent);
164         mInstrumentClusterHelperReady = true;
165         if (mClusterDisplayId != INVALID_DISPLAY) {
166             mHandler.post(mLaunchMainActivity);
167         }
168         return binder;
169     }
170 
171     @Override
onCreate()172     public void onCreate() {
173         super.onCreate();
174         Log.d(TAG, "onCreate");
175         mDisplayProvider = new ClusterDisplayProvider(this, mDisplayListener);
176 
177         mUserReceiver.register(this);
178     }
179 
onDestroy()180     public void onDestroy() {
181         super.onDestroy();
182         mUserReceiver.unregister(this);
183     }
184 
launchMainActivity()185     private void launchMainActivity() {
186         mHandler.removeCallbacks(mLaunchMainActivity);
187         ActivityOptions options = ActivityOptions.makeBasic();
188         options.setLaunchDisplayId(mClusterDisplayId);
189         boolean useNavigationOnly = getResources().getBoolean(R.bool.navigationOnly);
190         Intent intent;
191         int userId = UserHandle.USER_SYSTEM;
192         if (useNavigationOnly) {
193             intent = getNavigationActivityIntent(mClusterDisplayId);
194             if (intent == null) {
195                 mHandler.postDelayed(mLaunchMainActivity, NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS);
196                 return;
197             }
198             userId = ActivityManager.getCurrentUser();
199             startFixedActivityModeForDisplayAndUser(intent, options, userId);
200         } else {
201             intent = getMainClusterActivityIntent();
202             startActivityAsUser(intent, options.toBundle(), UserHandle.SYSTEM);
203         }
204         Log.i(TAG, "launching main activity=" + intent + ", display=" + mClusterDisplayId
205                 + ", userId=" + userId);
206     }
207 
getMainClusterActivityIntent()208     private Intent getMainClusterActivityIntent() {
209         return new Intent(this, MainClusterActivity.class).setFlags(FLAG_ACTIVITY_NEW_TASK);
210     }
211 
getNavigationActivityIntent(int displayId)212     private Intent getNavigationActivityIntent(int displayId) {
213         ComponentName component = MainClusterActivity.getNavigationActivity(this);
214         if (component == null) {
215             Log.e(TAG, "Failed to resolve the navigation activity");
216             return null;
217         }
218         Rect displaySize = new Rect(0, 0, 320, 240);  // Arbitrary size, better than nothing.
219         DisplayManager dm = (DisplayManager) getSystemService(DisplayManager.class);
220         Display display = dm.getDisplay(displayId);
221         if (display != null) {
222             display.getRectSize(displaySize);
223         }
224         return new Intent(Intent.ACTION_MAIN)
225             .setComponent(component)
226             .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
227             .putExtra(CarInstrumentClusterManager.KEY_EXTRA_ACTIVITY_STATE,
228                 ClusterActivityState.create(/* visible= */ true,
229                     /* unobscuredBounds= */ displaySize).toBundle());
230     }
231 
232     @Override
onKeyEvent(KeyEvent keyEvent)233     public void onKeyEvent(KeyEvent keyEvent) {
234         if (Log.isLoggable(TAG, Log.DEBUG)) {
235             Log.d(TAG, "onKeyEvent, keyEvent: " + keyEvent);
236         }
237         broadcastClientEvent(client -> client.onKeyEvent(keyEvent));
238     }
239 
240     /**
241      * Broadcasts an event to all the registered service clients
242      *
243      * @param event event to broadcast
244      */
broadcastClientEvent(Consumer<ServiceClient> event)245     private void broadcastClientEvent(Consumer<ServiceClient> event) {
246         for (ServiceClient client : mClients) {
247             event.accept(client);
248         }
249     }
250 
251     @Override
getNavigationRenderer()252     public NavigationRenderer getNavigationRenderer() {
253         NavigationRenderer navigationRenderer = new NavigationRenderer() {
254             @Override
255             public CarNavigationInstrumentCluster getNavigationProperties() {
256                 CarNavigationInstrumentCluster config =
257                         CarNavigationInstrumentCluster.createCluster(1000);
258                 Log.d(TAG, "getNavigationProperties, returns: " + config);
259                 return config;
260             }
261 
262             @Override
263             public void onNavigationStateChanged(Bundle bundle) {
264                 StringBuilder bundleSummary = new StringBuilder();
265 
266                 // Attempt to read proto byte array
267                 byte[] protoBytes = bundle.getByteArray(NAV_STATE_PROTO_BUNDLE_KEY);
268                 if (protoBytes != null) {
269                     try {
270                         NavigationStateProto navState = NavigationStateProto.parseFrom(
271                                 protoBytes);
272                         bundleSummary.append(navState.toString());
273 
274                         // Update clients
275                         broadcastClientEvent(
276                                 client -> client.onNavigationStateChange(navState));
277                     } catch (InvalidProtocolBufferException e) {
278                         Log.e(TAG, "Error parsing navigation state proto", e);
279                     }
280                 } else {
281                     Log.e(TAG, "Received nav state byte array is null");
282                 }
283                 Log.d(TAG, "onNavigationStateChanged(" + bundleSummary + ")");
284             }
285         };
286 
287         Log.i(TAG, "createNavigationRenderer, returns: " + navigationRenderer);
288         return navigationRenderer;
289     }
290 
291     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)292     protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
293         if (args != null && args.length > 0) {
294             execShellCommand(args);
295         } else {
296             super.dump(fd, writer, args);
297             writer.println("DisplayProvider: " + mDisplayProvider);
298         }
299     }
300 
emulateKeyEvent(int keyCode)301     private void emulateKeyEvent(int keyCode) {
302         Log.i(TAG, "emulateKeyEvent, keyCode: " + keyCode);
303         long downTime = SystemClock.uptimeMillis();
304         long eventTime = SystemClock.uptimeMillis();
305         KeyEvent event = obtainKeyEvent(keyCode, downTime, eventTime, KeyEvent.ACTION_DOWN);
306         onKeyEvent(event);
307 
308         eventTime = SystemClock.uptimeMillis();
309         event = obtainKeyEvent(keyCode, downTime, eventTime, KeyEvent.ACTION_UP);
310         onKeyEvent(event);
311     }
312 
obtainKeyEvent(int keyCode, long downTime, long eventTime, int action)313     private KeyEvent obtainKeyEvent(int keyCode, long downTime, long eventTime, int action) {
314         int scanCode = 0;
315         if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
316             scanCode = 108;
317         } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
318             scanCode = 106;
319         }
320         return KeyEvent.obtain(
321                 downTime,
322                 eventTime,
323                 action,
324                 keyCode,
325                 0 /* repeat */,
326                 0 /* meta state */,
327                 0 /* deviceId*/,
328                 scanCode /* scancode */,
329                 KeyEvent.FLAG_FROM_SYSTEM /* flags */,
330                 InputDevice.SOURCE_KEYBOARD,
331                 null /* characters */);
332     }
333 
execShellCommand(String[] args)334     private void execShellCommand(String[] args) {
335         Log.i(TAG, "execShellCommand, args: " + Arrays.toString(args));
336 
337         String command = args[0];
338 
339         switch (command) {
340             case "injectKey": {
341                 if (args.length > 1) {
342                     emulateKeyEvent(parseInt(args[1]));
343                 } else {
344                     Log.i(TAG, "Not enough arguments");
345                 }
346                 break;
347             }
348             case "destroyOverlayDisplay": {
349                 Settings.Global.putString(getContentResolver(),
350                         Global.OVERLAY_DISPLAY_DEVICES, "");
351                 break;
352             }
353 
354             case "createOverlayDisplay": {
355                 if (args.length > 1) {
356                     Settings.Global.putString(getContentResolver(),
357                             Global.OVERLAY_DISPLAY_DEVICES, args[1]);
358                 } else {
359                     Log.i(TAG, "Not enough arguments, expected 2");
360                 }
361                 break;
362             }
363 
364             case "setUnobscuredArea": {
365                 if (args.length > 5) {
366                     Rect unobscuredArea = new Rect(parseInt(args[2]), parseInt(args[3]),
367                             parseInt(args[4]), parseInt(args[5]));
368                     try {
369                         setClusterActivityState(args[1],
370                                 ClusterActivityState.create(true, unobscuredArea).toBundle());
371                     } catch (CarNotConnectedException e) {
372                         Log.i(TAG, "Failed to set activity state.", e);
373                     }
374                 } else {
375                     Log.i(TAG, "wrong format, expected: category left top right bottom");
376                 }
377             }
378         }
379     }
380 
381     private class UserReceiver extends BroadcastReceiver {
register(Context context)382         void register(Context context) {
383             IntentFilter intentFilter = new IntentFilter(ACTION_USER_UNLOCKED);
384             context.registerReceiverAsUser(this, UserHandle.ALL, intentFilter, null, null);
385         }
386 
unregister(Context context)387         void unregister(Context context) {
388             context.unregisterReceiver(this);
389         }
390 
391         @Override
onReceive(Context context, Intent intent)392         public void onReceive(Context context, Intent intent) {
393             if (Log.isLoggable(TAG, Log.DEBUG)) {
394                 Log.d(TAG, "Broadcast received: " + intent);
395             }
396             int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
397             if (userId == ActivityManager.getCurrentUser() &&
398                 mInstrumentClusterHelperReady && mClusterDisplayId != INVALID_DISPLAY) {
399                 mHandler.post(mLaunchMainActivity);
400             }
401         }
402     }
403 }
404