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