blob: 9f4fee8b00ab964d3839a35034123be02c6abc66 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.translation;
import static android.view.translation.TranslationManager.EXTRA_CAPABILITIES;
import static android.view.translation.UiTranslationManager.EXTRA_SOURCE_LOCALE;
import static android.view.translation.UiTranslationManager.EXTRA_STATE;
import static android.view.translation.UiTranslationManager.EXTRA_TARGET_LOCALE;
import static android.view.translation.UiTranslationManager.STATE_UI_TRANSLATION_FINISHED;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.os.Bundle;
import android.os.IBinder;
import android.os.IRemoteCallback;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.service.translation.TranslationServiceInfo;
import android.util.ArraySet;
import android.util.Slog;
import android.view.autofill.AutofillId;
import android.view.inputmethod.InputMethodInfo;
import android.view.translation.ITranslationServiceCallback;
import android.view.translation.TranslationCapability;
import android.view.translation.TranslationContext;
import android.view.translation.TranslationSpec;
import android.view.translation.UiTranslationManager.UiTranslationState;
import android.view.translation.UiTranslationSpec;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.IResultReceiver;
import com.android.internal.os.TransferPipe;
import com.android.server.LocalServices;
import com.android.server.infra.AbstractPerUserSystemService;
import com.android.server.inputmethod.InputMethodManagerInternal;
import com.android.server.wm.ActivityTaskManagerInternal;
import com.android.server.wm.ActivityTaskManagerInternal.ActivityTokens;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.List;
final class TranslationManagerServiceImpl extends
AbstractPerUserSystemService<TranslationManagerServiceImpl, TranslationManagerService> {
private static final String TAG = "TranslationManagerServiceImpl";
@GuardedBy("mLock")
@Nullable
private RemoteTranslationService mRemoteTranslationService;
@GuardedBy("mLock")
@Nullable
private ServiceInfo mRemoteTranslationServiceInfo;
@GuardedBy("mLock")
private TranslationServiceInfo mTranslationServiceInfo;
@GuardedBy("mLock")
private WeakReference<ActivityTokens> mLastActivityTokens;
private ActivityTaskManagerInternal mActivityTaskManagerInternal;
private final TranslationServiceRemoteCallback mRemoteServiceCallback =
new TranslationServiceRemoteCallback();
private final RemoteCallbackList<IRemoteCallback> mTranslationCapabilityCallbacks =
new RemoteCallbackList<>();
private final ArraySet<IBinder> mWaitingFinishedCallbackActivities = new ArraySet();
protected TranslationManagerServiceImpl(
@NonNull TranslationManagerService master,
@NonNull Object lock, int userId, boolean disabled) {
super(master, lock, userId);
updateRemoteServiceLocked();
mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
}
@GuardedBy("mLock")
@Override // from PerUserSystemService
protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent)
throws PackageManager.NameNotFoundException {
mTranslationServiceInfo = new TranslationServiceInfo(getContext(),
serviceComponent, isTemporaryServiceSetLocked(), mUserId);
mRemoteTranslationServiceInfo = mTranslationServiceInfo.getServiceInfo();
return mTranslationServiceInfo.getServiceInfo();
}
@GuardedBy("mLock")
@Override // from PerUserSystemService
protected boolean updateLocked(boolean disabled) {
final boolean enabledChanged = super.updateLocked(disabled);
updateRemoteServiceLocked();
return enabledChanged;
}
/**
* Updates the reference to the remote service.
*/
@GuardedBy("mLock")
private void updateRemoteServiceLocked() {
if (mRemoteTranslationService != null) {
if (mMaster.debug) Slog.d(TAG, "updateRemoteService(): destroying old remote service");
mRemoteTranslationService.unbind();
mRemoteTranslationService = null;
}
}
@GuardedBy("mLock")
@Nullable
private RemoteTranslationService ensureRemoteServiceLocked() {
if (mRemoteTranslationService == null) {
final String serviceName = getComponentNameLocked();
if (serviceName == null) {
if (mMaster.verbose) {
Slog.v(TAG, "ensureRemoteServiceLocked(): no service component name.");
}
return null;
}
final ComponentName serviceComponent = ComponentName.unflattenFromString(serviceName);
mRemoteTranslationService = new RemoteTranslationService(getContext(), serviceComponent,
mUserId, /* isInstantAllowed= */ false, mRemoteServiceCallback);
}
return mRemoteTranslationService;
}
@GuardedBy("mLock")
void onTranslationCapabilitiesRequestLocked(@TranslationSpec.DataFormat int sourceFormat,
@TranslationSpec.DataFormat int destFormat,
@NonNull ResultReceiver resultReceiver) {
final RemoteTranslationService remoteService = ensureRemoteServiceLocked();
if (remoteService != null) {
remoteService.onTranslationCapabilitiesRequest(sourceFormat, destFormat,
resultReceiver);
}
}
public void registerTranslationCapabilityCallback(IRemoteCallback callback, int sourceUid) {
mTranslationCapabilityCallbacks.register(callback, sourceUid);
ensureRemoteServiceLocked();
}
public void unregisterTranslationCapabilityCallback(IRemoteCallback callback) {
mTranslationCapabilityCallbacks.unregister(callback);
}
@GuardedBy("mLock")
void onSessionCreatedLocked(@NonNull TranslationContext translationContext, int sessionId,
IResultReceiver resultReceiver) {
final RemoteTranslationService remoteService = ensureRemoteServiceLocked();
if (remoteService != null) {
remoteService.onSessionCreated(translationContext, sessionId, resultReceiver);
}
}
private int getActivityUidByComponentName(Context context, ComponentName componentName,
int userId) {
int translationActivityUid = -1;
try {
if (componentName != null) {
translationActivityUid = context.getPackageManager().getApplicationInfoAsUser(
componentName.getPackageName(), 0, userId).uid;
}
} catch (PackageManager.NameNotFoundException e) {
Slog.d(TAG, "Cannot find packageManager for" + componentName);
}
return translationActivityUid;
}
@GuardedBy("mLock")
public void onTranslationFinishedLocked(boolean activityDestroyed, IBinder token,
ComponentName componentName) {
int translationActivityUid =
getActivityUidByComponentName(getContext(), componentName, getUserId());
if (activityDestroyed) {
// In the Activity destroy case, we only calls onTranslationFinished() in
// non-finisTranslation() state. If there is a finisTranslation() calls by apps, we
// should remove the waiting callback to avoid callback twice.
invokeCallbacks(STATE_UI_TRANSLATION_FINISHED, /* sourceSpec= */
null, /* targetSpec= */null, translationActivityUid);
mWaitingFinishedCallbackActivities.remove(token);
} else {
if (mWaitingFinishedCallbackActivities.contains(token)) {
invokeCallbacks(STATE_UI_TRANSLATION_FINISHED, /* sourceSpec= */
null, /* targetSpec= */null, translationActivityUid);
mWaitingFinishedCallbackActivities.remove(token);
}
}
}
@GuardedBy("mLock")
public void updateUiTranslationStateLocked(@UiTranslationState int state,
TranslationSpec sourceSpec, TranslationSpec targetSpec, List<AutofillId> viewIds,
IBinder token, int taskId, UiTranslationSpec uiTranslationSpec) {
// Get top activity for a given task id
final ActivityTokens taskTopActivityTokens =
mActivityTaskManagerInternal.getTopActivityForTask(taskId);
if (taskTopActivityTokens == null
|| taskTopActivityTokens.getShareableActivityToken() != token) {
Slog.w(TAG, "Unknown activity or it was finished to query for update translation "
+ "state for token=" + token + " taskId=" + taskId + " for state= " + state);
return;
}
if (state == STATE_UI_TRANSLATION_FINISHED) {
mWaitingFinishedCallbackActivities.add(token);
}
int translationActivityUid = -1;
try {
IBinder activityToken = taskTopActivityTokens.getActivityToken();
taskTopActivityTokens.getApplicationThread().updateUiTranslationState(
activityToken, state, sourceSpec, targetSpec,
viewIds, uiTranslationSpec);
mLastActivityTokens = new WeakReference<>(taskTopActivityTokens);
ComponentName componentName =
mActivityTaskManagerInternal.getActivityName(activityToken);
translationActivityUid =
getActivityUidByComponentName(getContext(), componentName, getUserId());
} catch (RemoteException e) {
Slog.w(TAG, "Update UiTranslationState fail: " + e);
}
if (state != STATE_UI_TRANSLATION_FINISHED) {
invokeCallbacks(state, sourceSpec, targetSpec, translationActivityUid);
}
}
@GuardedBy("mLock")
public void dumpLocked(String prefix, FileDescriptor fd, PrintWriter pw) {
if (mLastActivityTokens != null) {
ActivityTokens activityTokens = mLastActivityTokens.get();
if (activityTokens == null) {
return;
}
try (TransferPipe tp = new TransferPipe()) {
activityTokens.getApplicationThread().dumpActivity(tp.getWriteFd(),
activityTokens.getActivityToken(), prefix,
new String[]{"--translation"});
tp.go(fd);
} catch (IOException e) {
pw.println(prefix + "Failure while dumping the activity: " + e);
} catch (RemoteException e) {
pw.println(prefix + "Got a RemoteException while dumping the activity");
}
} else {
pw.print(prefix); pw.println("No requested UiTranslation Activity.");
}
final int waitingFinishCallbackSize = mWaitingFinishedCallbackActivities.size();
if (waitingFinishCallbackSize > 0) {
pw.print(prefix); pw.print("number waiting finish callback activities: ");
pw.println(waitingFinishCallbackSize);
for (IBinder activityToken : mWaitingFinishedCallbackActivities) {
pw.print(prefix); pw.print("activityToken: "); pw.println(activityToken);
}
}
}
private void invokeCallbacks(
int state, TranslationSpec sourceSpec, TranslationSpec targetSpec,
int translationActivityUid) {
Bundle res = new Bundle();
res.putInt(EXTRA_STATE, state);
// TODO(177500482): Store the locale pair so it can be sent for RESUME events.
if (sourceSpec != null) {
res.putSerializable(EXTRA_SOURCE_LOCALE, sourceSpec.getLocale());
res.putSerializable(EXTRA_TARGET_LOCALE, targetSpec.getLocale());
}
// TODO(177500482): Only support the *current* Input Method.
List<InputMethodInfo> enabledInputMethods =
LocalServices.getService(InputMethodManagerInternal.class)
.getEnabledInputMethodListAsUser(mUserId);
mCallbacks.broadcast((callback, uid) -> {
if ((int) uid == translationActivityUid) {
try {
callback.sendResult(res);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to invoke UiTranslationStateCallback: " + e);
}
}
// Code here is non-optimal since it's temporary..
boolean isIme = false;
for (InputMethodInfo inputMethod : enabledInputMethods) {
if ((int) uid == inputMethod.getServiceInfo().applicationInfo.uid) {
isIme = true;
}
}
// TODO(177500482): Invoke it for the application being translated too.
if (!isIme) {
return;
}
try {
callback.sendResult(res);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to invoke UiTranslationStateCallback: " + e);
}
});
}
public void registerUiTranslationStateCallback(IRemoteCallback callback, int sourceUid) {
mCallbacks.register(callback, sourceUid);
// TODO(177500482): trigger the callback here if we're already translating the UI.
}
public void unregisterUiTranslationStateCallback(IRemoteCallback callback) {
mCallbacks.unregister(callback);
}
private final RemoteCallbackList<IRemoteCallback> mCallbacks = new RemoteCallbackList<>();
public ComponentName getServiceSettingsActivityLocked() {
if (mTranslationServiceInfo == null) {
return null;
}
final String activityName = mTranslationServiceInfo.getSettingsActivity();
if (activityName == null) {
return null;
}
final String packageName = mTranslationServiceInfo.getServiceInfo().packageName;
return new ComponentName(packageName, activityName);
}
private void notifyClientsTranslationCapability(TranslationCapability capability) {
final Bundle res = new Bundle();
res.putParcelable(EXTRA_CAPABILITIES, capability);
mTranslationCapabilityCallbacks.broadcast((callback, uid) -> {
try {
callback.sendResult(res);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to invoke UiTranslationStateCallback: " + e);
}
});
}
private final class TranslationServiceRemoteCallback extends
ITranslationServiceCallback.Stub {
@Override
public void updateTranslationCapability(TranslationCapability capability) {
if (capability == null) {
Slog.wtf(TAG, "received a null TranslationCapability from TranslationService.");
return;
}
notifyClientsTranslationCapability(capability);
}
}
}