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