1 /*
2  * Copyright (C) 2016 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 com.google.android.car.kitchensink.cluster;
17 
18 import android.annotation.Nullable;
19 import android.car.Car;
20 import android.car.Car.CarServiceLifecycleListener;
21 import android.car.CarAppFocusManager;
22 import android.car.CarNotConnectedException;
23 import android.car.cluster.navigation.NavigationState;
24 import android.car.cluster.navigation.NavigationState.Cue;
25 import android.car.cluster.navigation.NavigationState.Cue.CueElement;
26 import android.car.cluster.navigation.NavigationState.Destination;
27 import android.car.cluster.navigation.NavigationState.Destination.Traffic;
28 import android.car.cluster.navigation.NavigationState.Distance;
29 import android.car.cluster.navigation.NavigationState.Lane;
30 import android.car.cluster.navigation.NavigationState.Lane.LaneDirection;
31 import android.car.cluster.navigation.NavigationState.Maneuver;
32 import android.car.cluster.navigation.NavigationState.NavigationStateProto;
33 import android.car.cluster.navigation.NavigationState.Road;
34 import android.car.cluster.navigation.NavigationState.Step;
35 import android.car.cluster.navigation.NavigationState.Timestamp;
36 import android.car.navigation.CarNavigationStatusManager;
37 import android.content.ComponentName;
38 import android.content.pm.PackageManager;
39 import android.os.Bundle;
40 import android.util.Log;
41 import android.view.LayoutInflater;
42 import android.view.View;
43 import android.view.ViewGroup;
44 import android.widget.Button;
45 import android.widget.RadioButton;
46 import android.widget.Toast;
47 
48 import androidx.annotation.IdRes;
49 import androidx.annotation.NonNull;
50 import androidx.fragment.app.Fragment;
51 
52 import com.google.android.car.kitchensink.R;
53 
54 import java.io.BufferedReader;
55 import java.io.IOException;
56 import java.io.InputStream;
57 import java.io.InputStreamReader;
58 import java.util.Timer;
59 import java.util.TimerTask;
60 
61 /**
62  * Contains functions to test instrument cluster API.
63  */
64 public class InstrumentClusterFragment extends Fragment {
65     private static final String TAG = "Cluster.KitchenSink";
66 
67     private static final int DISPLAY_IN_CLUSTER_PERMISSION_REQUEST = 1;
68 
69     private CarNavigationStatusManager mCarNavigationStatusManager;
70     private CarAppFocusManager mCarAppFocusManager;
71     private Car mCarApi;
72     private Timer mTimer;
73     private NavigationStateProto[] mNavStateData;
74     private Button mTurnByTurnButton;
75 
76     private CarServiceLifecycleListener mCarServiceLifecycleListener = (car, ready) -> {
77         if (!ready) {
78             Log.d(TAG, "Disconnect from Car Service");
79             return;
80         }
81         Log.d(TAG, "Connected to Car Service");
82         try {
83             mCarNavigationStatusManager = (CarNavigationStatusManager) car.getCarManager(
84                     Car.CAR_NAVIGATION_SERVICE);
85             mCarAppFocusManager = (CarAppFocusManager) car.getCarManager(
86                     Car.APP_FOCUS_SERVICE);
87         } catch (CarNotConnectedException e) {
88             Log.e(TAG, "Car is not connected!", e);
89         }
90     };
91 
92     private final CarAppFocusManager.OnAppFocusOwnershipCallback mFocusCallback =
93             new CarAppFocusManager.OnAppFocusOwnershipCallback() {
94                 @Override
95                 public void onAppFocusOwnershipLost(@CarAppFocusManager.AppFocusType int appType) {
96                     if (Log.isLoggable(TAG, Log.DEBUG)) {
97                         Log.d(TAG, "onAppFocusOwnershipLost, appType: " + appType);
98                     }
99                     Toast.makeText(getContext(), getText(R.string.cluster_nav_app_context_loss),
100                             Toast.LENGTH_LONG).show();
101                 }
102 
103                 @Override
104                 public void onAppFocusOwnershipGranted(
105                         @CarAppFocusManager.AppFocusType int appType) {
106                     if (Log.isLoggable(TAG, Log.DEBUG)) {
107                         Log.d(TAG, "onAppFocusOwnershipGranted, appType: " + appType);
108                     }
109                 }
110             };
111     private CarAppFocusManager.OnAppFocusChangedListener mOnAppFocusChangedListener =
112             (appType, active) -> {
113                 if (Log.isLoggable(TAG, Log.DEBUG)) {
114                     Log.d(TAG, "onAppFocusChanged, appType: " + appType + " active: " + active);
115                 }
116             };
117 
118 
initCarApi()119     private void initCarApi() {
120         mCarApi = Car.createCar(getContext(), /* handler= */ null,
121                 Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, mCarServiceLifecycleListener);
122     }
123 
124     @NonNull
getNavStateData()125     private NavigationStateProto[] getNavStateData() {
126         NavigationStateProto[] navigationStateArray = new NavigationStateProto[1];
127 
128         navigationStateArray[0] = NavigationStateProto.newBuilder()
129                 .setServiceStatus(NavigationStateProto.ServiceStatus.NORMAL)
130                 .addSteps(Step.newBuilder()
131                         .setManeuver(Maneuver.newBuilder()
132                                 .setType(Maneuver.Type.DEPART)
133                                 .build())
134                         .setDistance(Distance.newBuilder()
135                                 .setMeters(300)
136                                 .setDisplayUnits(Distance.Unit.FEET)
137                                 .setDisplayValue("0.5")
138                                 .build())
139                         .setCue(Cue.newBuilder()
140                                 .addElements(CueElement.newBuilder()
141                                         .setText("Stay on ")
142                                         .build())
143                                 .addElements(CueElement.newBuilder()
144                                         .setText("US 101 ")
145                                         .setImage(NavigationState.ImageReference.newBuilder()
146                                                 .setAspectRatio(1.153846)
147                                                 .setContentUri(
148                                                         "content://com.google.android.car"
149                                                                 + ".kitchensink.cluster"
150                                                                 + ".clustercontentprovider/img"
151                                                                 + "/US_101.png")
152                                                 .build())
153                                         .build())
154                                 .build())
155                         .addLanes(Lane.newBuilder()
156                                 .addLaneDirections(LaneDirection.newBuilder()
157                                         .setShape(LaneDirection.Shape.SLIGHT_LEFT)
158                                         .setIsHighlighted(false)
159                                         .build())
160                                 .addLaneDirections(LaneDirection.newBuilder()
161                                         .setShape(LaneDirection.Shape.STRAIGHT)
162                                         .setIsHighlighted(true)
163                                         .build())
164                                 .build())
165                         .build())
166                 .setCurrentRoad(Road.newBuilder()
167                         .setName("On something really long st")
168                         .build())
169                 .addDestinations(Destination.newBuilder()
170                         .setTitle("Home")
171                         .setAddress("123 Main st")
172                         .setDistance(Distance.newBuilder()
173                                 .setMeters(2000)
174                                 .setDisplayValue("2")
175                                 .setDisplayUnits(Distance.Unit.KILOMETERS)
176                                 .build())
177                         .setEstimatedTimeAtArrival(Timestamp.newBuilder()
178                                 .setSeconds(1592610807)
179                                 .build())
180                         .setFormattedDurationUntilArrival("45 min")
181                         .setZoneId("America/Los_Angeles")
182                         .setTraffic(Traffic.HIGH)
183                         .build())
184                 .build();
185 
186         return navigationStateArray;
187     }
188 
189     /**
190      * Loads a raw resource as a single string.
191      */
192     @NonNull
getRawResourceAsString(@dRes int resId)193     private String getRawResourceAsString(@IdRes int resId) throws IOException {
194         InputStream inputStream = getResources().openRawResource(resId);
195         BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
196         StringBuilder builder = new StringBuilder();
197         for (String line; (line = reader.readLine()) != null; ) {
198             builder.append(line).append("\n");
199         }
200         return builder.toString();
201     }
202 
203     @Nullable
204     @Override
onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)205     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
206             @Nullable Bundle savedInstanceState) {
207         View view = inflater.inflate(R.layout.instrument_cluster, container, false);
208 
209         view.findViewById(R.id.cluster_start_button).setOnClickListener(v -> initCluster());
210         view.findViewById(R.id.cluster_stop_button).setOnClickListener(v -> stopCluster());
211         view.findViewById(R.id.cluster_activity_state_default).setOnClickListener(v ->
212                 changeClusterActivityState(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT));
213         view.findViewById(R.id.cluster_activity_state_enabled).setOnClickListener(v ->
214                 changeClusterActivityState(PackageManager.COMPONENT_ENABLED_STATE_ENABLED));
215         view.findViewById(R.id.cluster_activity_state_disabled).setOnClickListener(v ->
216                 changeClusterActivityState(PackageManager.COMPONENT_ENABLED_STATE_DISABLED));
217         updateInitialClusterActivityState(view);
218 
219         mTurnByTurnButton = view.findViewById(R.id.cluster_turn_left_button);
220         mTurnByTurnButton.setOnClickListener(v -> toggleSendTurn());
221 
222         return view;
223     }
224 
updateInitialClusterActivityState(View view)225     private void updateInitialClusterActivityState(View view) {
226         PackageManager pm = getContext().getPackageManager();
227         ComponentName clusterActivity =
228                 new ComponentName(getContext(), FakeClusterNavigationActivity.class);
229         int currentComponentState = pm.getComponentEnabledSetting(clusterActivity);
230         RadioButton button = view.findViewById(
231                 convertClusterActivityStateToViewId(currentComponentState));
232         button.setChecked(true);
233     }
234 
convertClusterActivityStateToViewId(int componentState)235     private int convertClusterActivityStateToViewId(int componentState) {
236         switch (componentState) {
237             case PackageManager.COMPONENT_ENABLED_STATE_DEFAULT:
238                 return R.id.cluster_activity_state_default;
239             case PackageManager.COMPONENT_ENABLED_STATE_ENABLED:
240                 return R.id.cluster_activity_state_enabled;
241             case PackageManager.COMPONENT_ENABLED_STATE_DISABLED:
242                 return R.id.cluster_activity_state_disabled;
243         }
244         throw new IllegalStateException("Unknown component state: " + componentState);
245     }
246 
changeClusterActivityState(int newComponentState)247     private void changeClusterActivityState(int newComponentState) {
248         PackageManager pm = getContext().getPackageManager();
249         ComponentName clusterActivity =
250                 new ComponentName(getContext(), FakeClusterNavigationActivity.class);
251         pm.setComponentEnabledSetting(clusterActivity, newComponentState,
252                 PackageManager.DONT_KILL_APP);
253     }
254 
255     @Override
onCreate(@ullable Bundle savedInstanceState)256     public void onCreate(@Nullable Bundle savedInstanceState) {
257         initCarApi();
258         super.onCreate(savedInstanceState);
259     }
260 
261     @Override
onDestroy()262     public void onDestroy() {
263         if (mCarApi != null && mCarApi.isConnected()) {
264             mCarApi.disconnect();
265             mCarApi = null;
266         }
267         super.onDestroy();
268     }
269 
270     /**
271      * Enables/disables sending turn-by-turn data through the {@link CarNavigationStatusManager}
272      */
toggleSendTurn()273     private void toggleSendTurn() {
274         // If we haven't yet load the sample navigation state data, do so.
275         if (mNavStateData == null) {
276             mNavStateData = getNavStateData();
277         }
278 
279         // Toggle a timer to send update periodically.
280         if (mTimer == null) {
281             startSendTurn();
282         } else {
283             stopSendTurn();
284         }
285     }
286 
startSendTurn()287     private void startSendTurn() {
288         if (mTimer != null) {
289             stopSendTurn();
290         }
291         if (!hasFocus()) {
292             Toast.makeText(getContext(), getText(R.string.cluster_not_started), Toast.LENGTH_LONG)
293                     .show();
294             return;
295         }
296         mTimer = new Timer();
297         mTimer.schedule(new TimerTask() {
298             private int mPos;
299 
300             @Override
301             public void run() {
302                 sendTurn(mNavStateData[mPos]);
303                 mPos = (mPos + 1) % mNavStateData.length;
304             }
305         }, 0, 1000);
306         mTurnByTurnButton.setText(R.string.cluster_stop_guidance);
307     }
308 
stopSendTurn()309     private void stopSendTurn() {
310         if (mTimer != null) {
311             mTimer.cancel();
312             mTimer = null;
313         }
314         sendTurn(NavigationStateProto.newBuilder().build());
315         mTurnByTurnButton.setText(R.string.cluster_start_guidance);
316     }
317 
318     /**
319      * Sends one update of the navigation state through the {@link CarNavigationStatusManager}
320      */
sendTurn(@onNull NavigationStateProto state)321     private void sendTurn(@NonNull NavigationStateProto state) {
322         if (hasFocus()) {
323             Bundle bundle = new Bundle();
324             bundle.putByteArray("navstate2", state.toByteArray());
325             mCarNavigationStatusManager.sendNavigationStateChange(bundle);
326             Log.i(TAG, "Sending nav state: " + state);
327         }
328     }
329 
initCluster()330     private void initCluster() {
331         if (hasFocus()) {
332             Log.i(TAG, "Already has focus");
333             return;
334         }
335         mCarAppFocusManager.addFocusListener(mOnAppFocusChangedListener,
336                 CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
337         mCarAppFocusManager.requestAppFocus(CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION,
338                 mFocusCallback);
339         Log.i(TAG, "Focus requested");
340     }
341 
hasFocus()342     private boolean hasFocus() {
343         boolean ownsFocus = mCarAppFocusManager.isOwningFocus(mFocusCallback,
344                 CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
345         if (Log.isLoggable(TAG, Log.DEBUG)) {
346             Log.d(TAG, "Owns APP_FOCUS_TYPE_NAVIGATION: " + ownsFocus);
347         }
348         return ownsFocus;
349     }
350 
stopCluster()351     private void stopCluster() {
352         stopSendTurn();
353         mCarAppFocusManager.removeFocusListener(mOnAppFocusChangedListener,
354                 CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
355         mCarAppFocusManager.abandonAppFocus(mFocusCallback,
356                 CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
357     }
358 
359     @Override
onResume()360     public void onResume() {
361         super.onResume();
362         Log.i(TAG, "onResume!");
363         if (getActivity().checkSelfPermission(android.car.Car.PERMISSION_CAR_DISPLAY_IN_CLUSTER)
364                 != PackageManager.PERMISSION_GRANTED) {
365             Log.i(TAG, "Requesting: " + android.car.Car.PERMISSION_CAR_DISPLAY_IN_CLUSTER);
366 
367             requestPermissions(new String[]{android.car.Car.PERMISSION_CAR_DISPLAY_IN_CLUSTER},
368                     DISPLAY_IN_CLUSTER_PERMISSION_REQUEST);
369         } else {
370             Log.i(TAG, "All required permissions granted");
371         }
372     }
373 
374     @Override
onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)375     public void onRequestPermissionsResult(int requestCode, String[] permissions,
376             int[] grantResults) {
377         if (DISPLAY_IN_CLUSTER_PERMISSION_REQUEST == requestCode) {
378             for (int i = 0; i < permissions.length; i++) {
379                 boolean granted = grantResults[i] == PackageManager.PERMISSION_GRANTED;
380                 Log.i(TAG, "onRequestPermissionsResult, requestCode: " + requestCode
381                         + ", permission: " + permissions[i] + ", granted: " + granted);
382             }
383         }
384     }
385 }
386