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