1 /* 2 * Copyright (C) 2020 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 17 package android.service.translation; 18 19 import static android.view.translation.TranslationManager.STATUS_SYNC_CALL_FAIL; 20 import static android.view.translation.TranslationManager.STATUS_SYNC_CALL_SUCCESS; 21 import static android.view.translation.Translator.EXTRA_SERVICE_BINDER; 22 import static android.view.translation.Translator.EXTRA_SESSION_ID; 23 24 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; 25 26 import android.annotation.CallSuper; 27 import android.annotation.NonNull; 28 import android.annotation.Nullable; 29 import android.annotation.SystemApi; 30 import android.app.Service; 31 import android.content.Intent; 32 import android.content.pm.ParceledListSlice; 33 import android.os.BaseBundle; 34 import android.os.Bundle; 35 import android.os.CancellationSignal; 36 import android.os.Handler; 37 import android.os.IBinder; 38 import android.os.ICancellationSignal; 39 import android.os.Looper; 40 import android.os.RemoteException; 41 import android.os.ResultReceiver; 42 import android.util.Log; 43 import android.view.translation.ITranslationDirectManager; 44 import android.view.translation.ITranslationServiceCallback; 45 import android.view.translation.TranslationCapability; 46 import android.view.translation.TranslationContext; 47 import android.view.translation.TranslationManager; 48 import android.view.translation.TranslationRequest; 49 import android.view.translation.TranslationResponse; 50 import android.view.translation.TranslationSpec; 51 import android.view.translation.Translator; 52 53 import com.android.internal.os.IResultReceiver; 54 55 import java.util.Arrays; 56 import java.util.Objects; 57 import java.util.Set; 58 import java.util.function.Consumer; 59 60 /** 61 * Service for translating text. 62 * @hide 63 */ 64 @SystemApi 65 public abstract class TranslationService extends Service { 66 private static final String TAG = "TranslationService"; 67 68 /** 69 * The {@link Intent} that must be declared as handled by the service. 70 * 71 * <p>To be supported, the service must also require the 72 * {@link android.Manifest.permission#BIND_TRANSLATION_SERVICE} permission so 73 * that other applications can not abuse it. 74 */ 75 public static final String SERVICE_INTERFACE = 76 "android.service.translation.TranslationService"; 77 78 /** 79 * Name under which a TranslationService component publishes information about itself. 80 * 81 * <p>This meta-data should reference an XML resource containing a 82 * <code><{@link 83 * android.R.styleable#TranslationService translation-service}></code> tag. 84 * 85 * <p>Here's an example of how to use it on {@code AndroidManifest.xml}: 86 * <pre> <translation-service 87 * android:settingsActivity="foo.bar.SettingsActivity" 88 * . . . 89 * /></pre> 90 */ 91 public static final String SERVICE_META_DATA = "android.translation_service"; 92 93 private Handler mHandler; 94 private ITranslationServiceCallback mCallback; 95 96 97 /** 98 * Binder to receive calls from system server. 99 */ 100 private final ITranslationService mInterface = new ITranslationService.Stub() { 101 @Override 102 public void onConnected(IBinder callback) { 103 mHandler.sendMessage(obtainMessage(TranslationService::handleOnConnected, 104 TranslationService.this, callback)); 105 } 106 107 @Override 108 public void onDisconnected() { 109 mHandler.sendMessage(obtainMessage(TranslationService::onDisconnected, 110 TranslationService.this)); 111 } 112 113 @Override 114 public void onCreateTranslationSession(TranslationContext translationContext, 115 int sessionId, IResultReceiver receiver) throws RemoteException { 116 mHandler.sendMessage(obtainMessage(TranslationService::handleOnCreateTranslationSession, 117 TranslationService.this, translationContext, sessionId, receiver)); 118 } 119 120 @Override 121 public void onTranslationCapabilitiesRequest(@TranslationSpec.DataFormat int sourceFormat, 122 @TranslationSpec.DataFormat int targetFormat, 123 @NonNull ResultReceiver resultReceiver) throws RemoteException { 124 mHandler.sendMessage( 125 obtainMessage(TranslationService::handleOnTranslationCapabilitiesRequest, 126 TranslationService.this, sourceFormat, targetFormat, 127 resultReceiver)); 128 } 129 }; 130 131 /** 132 * Interface definition for a callback to be invoked when the translation is compleled. 133 * @removed use a {@link Consumer} instead. 134 */ 135 @Deprecated 136 public interface OnTranslationResultCallback { 137 /** 138 * Notifies the Android System that a translation request 139 * {@link TranslationService#onTranslationRequest(TranslationRequest, int, 140 * CancellationSignal, OnTranslationResultCallback)} was successfully fulfilled by the 141 * service. 142 * 143 * <p>This method should always be called, even if the service cannot fulfill the request 144 * (in which case it should be called with a TranslationResponse with 145 * {@link android.view.translation.TranslationResponse#TRANSLATION_STATUS_UNKNOWN_ERROR}, 146 * or {@link android.view.translation.TranslationResponse 147 * #TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE}). 148 * 149 * @param response translation response for the provided request infos. 150 * 151 * @throws IllegalStateException if this method was already called. 152 */ onTranslationSuccess(@onNull TranslationResponse response)153 void onTranslationSuccess(@NonNull TranslationResponse response); 154 155 /** 156 * @removed use {@link #onTranslationSuccess} with an error response instead. 157 */ 158 @Deprecated onError()159 void onError(); 160 } 161 162 /** 163 * Binder that receives calls from the app. 164 */ 165 private final ITranslationDirectManager mClientInterface = 166 new ITranslationDirectManager.Stub() { 167 @Override 168 public void onTranslationRequest(TranslationRequest request, int sessionId, 169 ICancellationSignal transport, ITranslationCallback callback) 170 throws RemoteException { 171 final Consumer<TranslationResponse> consumer = 172 new OnTranslationResultCallbackWrapper(callback); 173 mHandler.sendMessage(obtainMessage(TranslationService::onTranslationRequest, 174 TranslationService.this, request, sessionId, 175 CancellationSignal.fromTransport(transport), 176 consumer)); 177 } 178 179 @Override 180 public void onFinishTranslationSession(int sessionId) throws RemoteException { 181 mHandler.sendMessage(obtainMessage( 182 TranslationService::onFinishTranslationSession, 183 TranslationService.this, sessionId)); 184 } 185 }; 186 187 @CallSuper 188 @Override onCreate()189 public void onCreate() { 190 super.onCreate(); 191 mHandler = new Handler(Looper.getMainLooper(), null, true); 192 BaseBundle.setShouldDefuse(true); 193 } 194 195 @Override 196 @Nullable onBind(@onNull Intent intent)197 public final IBinder onBind(@NonNull Intent intent) { 198 if (SERVICE_INTERFACE.equals(intent.getAction())) { 199 return mInterface.asBinder(); 200 } 201 Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent); 202 return null; 203 } 204 205 /** 206 * Called when the Android system connects to service. 207 * 208 * <p>You should generally do initialization here rather than in {@link #onCreate}. 209 */ onConnected()210 public void onConnected() { 211 } 212 213 /** 214 * Called when the Android system disconnects from the service. 215 * 216 * <p> At this point this service may no longer be an active {@link TranslationService}. 217 * It should not make calls on {@link TranslationManager} that requires the caller to be 218 * the current service. 219 */ onDisconnected()220 public void onDisconnected() { 221 } 222 223 /** 224 * Called to notify the service that a session was created 225 * (see {@link android.view.translation.Translator}). 226 * 227 * <p>The service must call {@code callback.accept()} to acknowledge whether the session is 228 * supported and created successfully. If the translation context is not supported, the service 229 * should call back with {@code false}.</p> 230 * 231 * @param translationContext the {@link TranslationContext} of the session being created. 232 * @param sessionId the id of the session. 233 * @param callback {@link Consumer} to notify whether the session was successfully created. 234 */ 235 // TODO(b/176464808): the session id won't be unique cross client/server process. Need to find 236 // solution to make it's safe. onCreateTranslationSession(@onNull TranslationContext translationContext, int sessionId, @NonNull Consumer<Boolean> callback)237 public abstract void onCreateTranslationSession(@NonNull TranslationContext translationContext, 238 int sessionId, @NonNull Consumer<Boolean> callback); 239 240 /** 241 * @removed use {@link #onCreateTranslationSession(TranslationContext, int, Consumer)} 242 * instead. 243 */ 244 @Deprecated onCreateTranslationSession(@onNull TranslationContext translationContext, int sessionId)245 public void onCreateTranslationSession(@NonNull TranslationContext translationContext, 246 int sessionId) { 247 // no-op 248 } 249 250 /** 251 * Called when a translation session is finished. 252 * 253 * <p>The translation session is finished when the client calls {@link Translator#destroy()} on 254 * the corresponding translator. 255 * 256 * @param sessionId id of the session that finished. 257 */ onFinishTranslationSession(int sessionId)258 public abstract void onFinishTranslationSession(int sessionId); 259 260 /** 261 * @removed use 262 * {@link #onTranslationRequest(TranslationRequest, int, CancellationSignal, Consumer)} instead. 263 */ 264 @Deprecated onTranslationRequest(@onNull TranslationRequest request, int sessionId, @Nullable CancellationSignal cancellationSignal, @NonNull OnTranslationResultCallback callback)265 public void onTranslationRequest(@NonNull TranslationRequest request, int sessionId, 266 @Nullable CancellationSignal cancellationSignal, 267 @NonNull OnTranslationResultCallback callback) { 268 // no-op 269 } 270 271 /** 272 * Called to the service with a {@link TranslationRequest} to be translated. 273 * 274 * <p>The service must call {@code callback.accept()} with the {@link TranslationResponse}. If 275 * {@link TranslationRequest#FLAG_PARTIAL_RESPONSES} was set, the service may call 276 * {@code callback.accept()} multiple times with partial responses.</p> 277 * 278 * @param request The translation request containing the data to be translated. 279 * @param sessionId id of the session that sent the translation request. 280 * @param cancellationSignal A {@link CancellationSignal} that notifies when a client has 281 * cancelled the operation in progress. 282 * @param callback {@link Consumer} to pass back the translation response. 283 */ onTranslationRequest(@onNull TranslationRequest request, int sessionId, @Nullable CancellationSignal cancellationSignal, @NonNull Consumer<TranslationResponse> callback)284 public abstract void onTranslationRequest(@NonNull TranslationRequest request, int sessionId, 285 @Nullable CancellationSignal cancellationSignal, 286 @NonNull Consumer<TranslationResponse> callback); 287 288 /** 289 * Called to request a set of {@link TranslationCapability}s that are supported by the service. 290 * 291 * <p>The set of translation capabilities are limited to those supporting the source and target 292 * {@link TranslationSpec.DataFormat}. e.g. Calling this with 293 * {@link TranslationSpec#DATA_FORMAT_TEXT} as source and target returns only capabilities that 294 * translates text to text.</p> 295 * 296 * <p>Must call {@code callback.accept} to pass back the set of translation capabilities.</p> 297 * 298 * @param sourceFormat data format restriction of the translation source spec. 299 * @param targetFormat data format restriction of the translation target spec. 300 * @param callback {@link Consumer} to pass back the set of translation capabilities. 301 */ onTranslationCapabilitiesRequest( @ranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, @NonNull Consumer<Set<TranslationCapability>> callback)302 public abstract void onTranslationCapabilitiesRequest( 303 @TranslationSpec.DataFormat int sourceFormat, 304 @TranslationSpec.DataFormat int targetFormat, 305 @NonNull Consumer<Set<TranslationCapability>> callback); 306 307 /** 308 * Called by the service to notify an update in existing {@link TranslationCapability}s. 309 * 310 * @param capability the updated {@link TranslationCapability} with its new states and flags. 311 */ updateTranslationCapability(@onNull TranslationCapability capability)312 public final void updateTranslationCapability(@NonNull TranslationCapability capability) { 313 Objects.requireNonNull(capability, "translation capability should not be null"); 314 315 final ITranslationServiceCallback callback = mCallback; 316 if (callback == null) { 317 Log.w(TAG, "updateTranslationCapability(): no server callback"); 318 return; 319 } 320 321 try { 322 callback.updateTranslationCapability(capability); 323 } catch (RemoteException e) { 324 e.rethrowFromSystemServer(); 325 } 326 } 327 handleOnConnected(@onNull IBinder callback)328 private void handleOnConnected(@NonNull IBinder callback) { 329 mCallback = ITranslationServiceCallback.Stub.asInterface(callback); 330 onConnected(); 331 } 332 333 // TODO(b/176464808): Need to handle client dying case 334 handleOnCreateTranslationSession(@onNull TranslationContext translationContext, int sessionId, IResultReceiver resultReceiver)335 private void handleOnCreateTranslationSession(@NonNull TranslationContext translationContext, 336 int sessionId, IResultReceiver resultReceiver) { 337 onCreateTranslationSession(translationContext, sessionId, 338 new Consumer<Boolean>() { 339 @Override 340 public void accept(Boolean created) { 341 try { 342 if (!created) { 343 Log.w(TAG, "handleOnCreateTranslationSession(): context=" 344 + translationContext + " not supported by service."); 345 resultReceiver.send(STATUS_SYNC_CALL_FAIL, null); 346 return; 347 } 348 349 final Bundle extras = new Bundle(); 350 extras.putBinder(EXTRA_SERVICE_BINDER, mClientInterface.asBinder()); 351 extras.putInt(EXTRA_SESSION_ID, sessionId); 352 resultReceiver.send(STATUS_SYNC_CALL_SUCCESS, extras); 353 } catch (RemoteException e) { 354 Log.w(TAG, "RemoteException sending client interface: " + e); 355 } 356 } 357 }); 358 359 } 360 handleOnTranslationCapabilitiesRequest( @ranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, @NonNull ResultReceiver resultReceiver)361 private void handleOnTranslationCapabilitiesRequest( 362 @TranslationSpec.DataFormat int sourceFormat, 363 @TranslationSpec.DataFormat int targetFormat, 364 @NonNull ResultReceiver resultReceiver) { 365 onTranslationCapabilitiesRequest(sourceFormat, targetFormat, 366 new Consumer<Set<TranslationCapability>>() { 367 @Override 368 public void accept(Set<TranslationCapability> values) { 369 if (!isValidCapabilities(sourceFormat, targetFormat, values)) { 370 throw new IllegalStateException("Invalid capabilities and " 371 + "format compatibility"); 372 } 373 374 final Bundle bundle = new Bundle(); 375 final ParceledListSlice<TranslationCapability> listSlice = 376 new ParceledListSlice<>(Arrays.asList( 377 values.toArray(new TranslationCapability[0]))); 378 bundle.putParcelable(TranslationManager.EXTRA_CAPABILITIES, listSlice); 379 resultReceiver.send(STATUS_SYNC_CALL_SUCCESS, bundle); 380 } 381 }); 382 } 383 384 /** 385 * Helper method to validate capabilities and format compatibility. 386 */ isValidCapabilities(@ranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, Set<TranslationCapability> capabilities)387 private boolean isValidCapabilities(@TranslationSpec.DataFormat int sourceFormat, 388 @TranslationSpec.DataFormat int targetFormat, Set<TranslationCapability> capabilities) { 389 if (sourceFormat != TranslationSpec.DATA_FORMAT_TEXT 390 && targetFormat != TranslationSpec.DATA_FORMAT_TEXT) { 391 return true; 392 } 393 394 for (TranslationCapability capability : capabilities) { 395 if (capability.getState() == TranslationCapability.STATE_REMOVED_AND_AVAILABLE) { 396 return false; 397 } 398 } 399 400 return true; 401 } 402 } 403