1 /*
2  * Copyright (C) 2021 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.android.car.cluster;
17 
18 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE;
19 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
20 
21 import android.car.Car;
22 import android.car.CarAppFocusManager;
23 import android.car.builtin.util.Slogf;
24 import android.car.cluster.navigation.NavigationState.Maneuver;
25 import android.car.cluster.navigation.NavigationState.NavigationStateProto;
26 import android.car.cluster.navigation.NavigationState.Step;
27 import android.car.cluster.renderer.IInstrumentClusterNavigation;
28 import android.car.navigation.CarNavigationInstrumentCluster;
29 import android.content.Context;
30 import android.os.Binder;
31 import android.os.Bundle;
32 import android.util.Log;
33 import android.util.proto.ProtoOutputStream;
34 
35 import com.android.car.AppFocusService;
36 import com.android.car.AppFocusService.FocusOwnershipCallback;
37 import com.android.car.CarLocalServices;
38 import com.android.car.CarLog;
39 import com.android.car.CarServiceBase;
40 import com.android.car.CarServiceUtils;
41 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
42 import com.android.car.internal.util.IndentingPrintWriter;
43 import com.android.internal.annotations.GuardedBy;
44 import com.android.internal.annotations.VisibleForTesting;
45 
46 import com.google.protobuf.InvalidProtocolBufferException;
47 
48 import java.util.Objects;
49 
50 /**
51  * Service responsible for Navigation focus management and {@code NavigationState} change.
52  *
53  * @hide
54  */
55 public class ClusterNavigationService extends IInstrumentClusterNavigation.Stub
56         implements CarServiceBase, FocusOwnershipCallback {
57 
58     @VisibleForTesting
59     static final String TAG = CarLog.TAG_CLUSTER;
60 
61     private static final ContextOwner NO_OWNER = new ContextOwner(0, 0);
62     private static final String NAV_STATE_PROTO_BUNDLE_KEY = "navstate2";
63 
64     private final Context mContext;
65     private final AppFocusService mAppFocusService;
66 
67     private final Object mLock = new Object();
68 
69     @GuardedBy("mLock")
70     private ContextOwner mNavContextOwner = NO_OWNER;
71 
72     interface ClusterNavigationServiceCallback {
onNavigationStateChanged(Bundle bundle)73         void onNavigationStateChanged(Bundle bundle);
74 
getInstrumentClusterInfo()75         CarNavigationInstrumentCluster getInstrumentClusterInfo();
76 
notifyNavContextOwnerChanged(ContextOwner owner)77         void notifyNavContextOwnerChanged(ContextOwner owner);
78     }
79 
80     @GuardedBy("mLock")
81     ClusterNavigationServiceCallback mClusterServiceCallback;
82 
83     @Override
onNavigationStateChanged(Bundle bundle)84     public void onNavigationStateChanged(Bundle bundle) {
85         CarServiceUtils.assertPermission(mContext, Car.PERMISSION_CAR_NAVIGATION_MANAGER);
86         assertNavigationFocus();
87         assertNavStateProtoValid(bundle);
88         ClusterNavigationServiceCallback callback;
89         synchronized (mLock) {
90             callback = mClusterServiceCallback;
91         }
92         if (callback == null) return;
93         callback.onNavigationStateChanged(bundle);
94     }
95 
96     @Override
getInstrumentClusterInfo()97     public CarNavigationInstrumentCluster getInstrumentClusterInfo() {
98         CarServiceUtils.assertPermission(mContext, Car.PERMISSION_CAR_NAVIGATION_MANAGER);
99         ClusterNavigationServiceCallback callback;
100         synchronized (mLock) {
101             callback = mClusterServiceCallback;
102         }
103         if (callback == null) return null;
104         return callback.getInstrumentClusterInfo();
105     }
106 
ClusterNavigationService(Context context, AppFocusService appFocusService)107     public ClusterNavigationService(Context context, AppFocusService appFocusService) {
108         mContext = context;
109         mAppFocusService = appFocusService;
110     }
111 
setClusterServiceCallback( ClusterNavigationServiceCallback clusterServiceCallback)112     public void setClusterServiceCallback(
113             ClusterNavigationServiceCallback clusterServiceCallback) {
114         synchronized (mLock) {
115             mClusterServiceCallback = clusterServiceCallback;
116         }
117     }
118 
119     @Override
init()120     public void init() {
121         if (Slogf.isLoggable(TAG, Log.DEBUG)) {
122             Slogf.d(TAG, "initClusterNavigationService");
123         }
124         mAppFocusService.registerContextOwnerChangedCallback(this /* FocusOwnershipCallback */);
125     }
126 
127     @Override
release()128     public void release() {
129         if (Slogf.isLoggable(TAG, Log.DEBUG)) {
130             Slogf.d(TAG, "releaseClusterNavigationService");
131         }
132         setClusterServiceCallback(null);
133         mAppFocusService.unregisterContextOwnerChangedCallback(this);
134     }
135 
136     @Override
137     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dump(IndentingPrintWriter writer)138     public void dump(IndentingPrintWriter writer) {
139         writer.println("**" + getClass().getSimpleName() + "**");
140         synchronized (mLock) {
141             writer.println("context owner: " + mNavContextOwner);
142         }
143     }
144 
145     @Override
146     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpProto(ProtoOutputStream proto)147     public void dumpProto(ProtoOutputStream proto) {}
148 
149     @Override
onFocusAcquired(int appType, int uid, int pid)150     public void onFocusAcquired(int appType, int uid, int pid) {
151         changeNavContextOwner(appType, uid, pid, true);
152     }
153 
154     @Override
onFocusAbandoned(int appType, int uid, int pid)155     public void onFocusAbandoned(int appType, int uid, int pid) {
156         changeNavContextOwner(appType, uid, pid, false);
157     }
158 
assertNavigationFocus()159     private void assertNavigationFocus() {
160         int uid = Binder.getCallingUid();
161         int pid = Binder.getCallingPid();
162         synchronized (mLock) {
163             if (uid == mNavContextOwner.uid && pid == mNavContextOwner.pid) {
164                 return;
165             }
166         }
167         // Stored one failed. There can be a delay, so check with real one again.
168         AppFocusService afs = CarLocalServices.getService(AppFocusService.class);
169         if (afs != null && afs.isFocusOwner(uid, pid,
170                 CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION)) {
171             return;
172         }
173         throw new IllegalStateException("Client not owning APP_FOCUS_TYPE_NAVIGATION");
174     }
175 
changeNavContextOwner(int appType, int uid, int pid, boolean acquire)176     private void changeNavContextOwner(int appType, int uid, int pid, boolean acquire) {
177         if (appType != CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION) {
178             return;
179         }
180         ContextOwner requester = new ContextOwner(uid, pid);
181         ContextOwner newOwner = acquire ? requester : NO_OWNER;
182         ClusterNavigationServiceCallback callback;
183         synchronized (mLock) {
184             if ((acquire && Objects.equals(mNavContextOwner, requester))
185                     || (!acquire && !Objects.equals(mNavContextOwner, requester))) {
186                 // Nothing to do here. Either the same owner is acquiring twice, or someone is
187                 // abandoning a focus they didn't have.
188                 Slogf.w(TAG, "Invalid nav context owner change (acquiring: " + acquire
189                         + "), current owner: [" + mNavContextOwner
190                         + "], requester: [" + requester + "]");
191                 return;
192             }
193 
194             mNavContextOwner = newOwner;
195             callback = mClusterServiceCallback;
196         }
197         if (callback == null) return;
198 
199         callback.notifyNavContextOwnerChanged(newOwner);
200     }
201 
assertNavStateProtoValid(Bundle bundle)202     private void assertNavStateProtoValid(Bundle bundle) {
203         byte[] protoBytes = bundle.getByteArray(NAV_STATE_PROTO_BUNDLE_KEY);
204         if (protoBytes == null) {
205             throw new IllegalArgumentException("Received navigation state byte array is null.");
206         }
207         try {
208             NavigationStateProto navigationStateProto = NavigationStateProto.parseFrom(protoBytes);
209             if (navigationStateProto.getStepsCount() == 0) {
210                 return;
211             }
212             for (Step step : navigationStateProto.getStepsList()) {
213                 Maneuver maneuver = step.getManeuver();
214                 if (!Maneuver.TypeV2.UNKNOWN_V2.equals(maneuver.getTypeV2())
215                         && Maneuver.Type.UNKNOWN.equals(maneuver.getType())) {
216                     throw new IllegalArgumentException(
217                         "Maneuver#type must be populated if Maneuver#typeV2 is also populated.");
218                 }
219             }
220         } catch (InvalidProtocolBufferException e) {
221             throw new IllegalArgumentException("Error parsing navigation state proto", e);
222         }
223     }
224 
225     static class ContextOwner {
226         final int uid;
227         final int pid;
228 
ContextOwner(int uid, int pid)229         ContextOwner(int uid, int pid) {
230             this.uid = uid;
231             this.pid = pid;
232         }
233 
234         @Override
toString()235         public String toString() {
236             return "uid: " + uid + ", pid: " + pid;
237         }
238 
239         @Override
equals(Object o)240         public boolean equals(Object o) {
241             if (this == o) return true;
242             if (o == null || getClass() != o.getClass()) return false;
243             ContextOwner that = (ContextOwner) o;
244             return uid == that.uid && pid == that.pid;
245         }
246 
247         @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE)
248         @Override
hashCode()249         public int hashCode() {
250             return Objects.hash(uid, pid);
251         }
252     }
253 }
254