Initial layout of translation related APIs.

The initial translation related APIs. The APIs and the implementation
will be revised in the follow up changes. The service register will
on the next changes.

Bug: 173243538
Bug: 176208267
Test: manual verification (build)

Change-Id: Ib7b39e1b548bc6663a81482fa335632e8da18d6a
diff --git a/services/Android.bp b/services/Android.bp
index 3447b5c..947bd00 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -30,6 +30,7 @@
         ":services.searchui-sources",
         ":services.startop.iorap-sources",
         ":services.systemcaptions-sources",
+        ":services.translation-sources",
         ":services.usage-sources",
         ":services.usb-sources",
         ":services.voiceinteraction-sources",
@@ -76,6 +77,7 @@
         "services.searchui",
         "services.startop",
         "services.systemcaptions",
+        "services.translation",
         "services.usage",
         "services.usb",
         "services.voiceinteraction",
diff --git a/services/translation/Android.bp b/services/translation/Android.bp
new file mode 100644
index 0000000..804a617
--- /dev/null
+++ b/services/translation/Android.bp
@@ -0,0 +1,13 @@
+filegroup {
+    name: "services.translation-sources",
+    srcs: ["java/**/*.java"],
+    path: "java",
+    visibility: ["//frameworks/base/services"],
+}
+
+java_library_static {
+    name: "services.translation",
+    defaults: ["platform_service_defaults"],
+    srcs: [":services.translation-sources"],
+    libs: ["services.core"],
+}
\ No newline at end of file
diff --git a/services/translation/OWNERS b/services/translation/OWNERS
new file mode 100644
index 0000000..a1e663a
--- /dev/null
+++ b/services/translation/OWNERS
@@ -0,0 +1,8 @@
+# Bug component: 994311
+
+adamhe@google.com
+augale@google.com
+joannechung@google.com
+lpeter@google.com
+svetoslavganov@google.com
+tymtsai@google.com
diff --git a/services/translation/java/com/android/server/translation/RemoteTranslationService.java b/services/translation/java/com/android/server/translation/RemoteTranslationService.java
new file mode 100644
index 0000000..0c7e617
--- /dev/null
+++ b/services/translation/java/com/android/server/translation/RemoteTranslationService.java
@@ -0,0 +1,87 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.service.translation.ITranslationService;
+import android.service.translation.TranslationService;
+import android.util.Slog;
+import android.view.translation.TranslationSpec;
+
+import com.android.internal.infra.AbstractRemoteService;
+import com.android.internal.infra.ServiceConnector;
+import com.android.internal.os.IResultReceiver;
+
+final class RemoteTranslationService extends ServiceConnector.Impl<ITranslationService> {
+
+    private static final String TAG = RemoteTranslationService.class.getSimpleName();
+
+    // TODO(b/176590870): Make PERMANENT now.
+    private static final long TIMEOUT_IDLE_UNBIND_MS =
+            AbstractRemoteService.PERMANENT_BOUND_TIMEOUT_MS;
+    private static final int TIMEOUT_REQUEST_MS = 5_000;
+
+    private final long mIdleUnbindTimeoutMs;
+    private final int mRequestTimeoutMs;
+    private final ComponentName mComponentName;
+
+    RemoteTranslationService(Context context, ComponentName serviceName,
+            int userId, boolean bindInstantServiceAllowed) {
+        super(context,
+                new Intent(TranslationService.SERVICE_INTERFACE).setComponent(serviceName),
+                bindInstantServiceAllowed ? Context.BIND_ALLOW_INSTANT : 0,
+                userId, ITranslationService.Stub::asInterface);
+        mIdleUnbindTimeoutMs = TIMEOUT_IDLE_UNBIND_MS;
+        mRequestTimeoutMs = TIMEOUT_REQUEST_MS;
+        mComponentName = serviceName;
+
+        // Bind right away.
+        connect();
+    }
+
+    public ComponentName getComponentName() {
+        return mComponentName;
+    }
+
+    @Override // from ServiceConnector.Impl
+    protected void onServiceConnectionStatusChanged(ITranslationService service,
+            boolean connected) {
+        try {
+            if (connected) {
+                service.onConnected();
+            } else {
+                service.onDisconnected();
+            }
+        } catch (Exception e) {
+            Slog.w(TAG,
+                    "Exception calling onServiceConnectionStatusChanged(" + connected + "): ", e);
+        }
+    }
+
+    @Override // from AbstractRemoteService
+    protected long getAutoDisconnectTimeoutMs() {
+        return mIdleUnbindTimeoutMs;
+    }
+
+    public void onSessionCreated(@NonNull TranslationSpec sourceSpec,
+            @NonNull TranslationSpec destSpec, int sessionId, IResultReceiver resultReceiver) {
+        run((s) -> s.onCreateTranslationSession(sourceSpec, destSpec, sessionId, resultReceiver));
+    }
+}
diff --git a/services/translation/java/com/android/server/translation/TranslationManagerService.java b/services/translation/java/com/android/server/translation/TranslationManagerService.java
new file mode 100644
index 0000000..e2aabe6
--- /dev/null
+++ b/services/translation/java/com/android/server/translation/TranslationManagerService.java
@@ -0,0 +1,92 @@
+/*
+ * 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.content.Context.TRANSLATION_MANAGER_SERVICE;
+import static android.view.translation.TranslationManager.STATUS_SYNC_CALL_FAIL;
+
+import android.content.Context;
+import android.os.RemoteException;
+import android.util.Slog;
+import android.view.translation.ITranslationManager;
+import android.view.translation.TranslationSpec;
+
+import com.android.internal.os.IResultReceiver;
+import com.android.server.infra.AbstractMasterSystemService;
+import com.android.server.infra.FrameworkResourcesServiceNameResolver;
+
+/**
+ * Entry point service for translation management.
+ *
+ * <p>This service provides the {@link ITranslationManager} implementation and keeps a list of
+ * {@link TranslationManagerServiceImpl} per user; the real work is done by
+ * {@link TranslationManagerServiceImpl} itself.
+ */
+public final class TranslationManagerService
+        extends AbstractMasterSystemService<TranslationManagerService,
+        TranslationManagerServiceImpl> {
+
+    private static final String TAG = "TranslationManagerService";
+
+    public TranslationManagerService(Context context) {
+        // TODO: Discuss the disallow policy
+        super(context, new FrameworkResourcesServiceNameResolver(context,
+                        com.android.internal.R.string.config_defaultTranslationService),
+                /* disallowProperty */ null, PACKAGE_UPDATE_POLICY_REFRESH_EAGER);
+    }
+
+    @Override
+    protected TranslationManagerServiceImpl newServiceLocked(int resolvedUserId, boolean disabled) {
+        return new TranslationManagerServiceImpl(this, mLock, resolvedUserId, disabled);
+    }
+
+    final class TranslationManagerServiceStub extends ITranslationManager.Stub {
+        @Override
+        public void getSupportedLocales(IResultReceiver receiver, int userId)
+                throws RemoteException {
+            synchronized (mLock) {
+                final TranslationManagerServiceImpl service = getServiceForUserLocked(userId);
+                if (service != null) {
+                    service.getSupportedLocalesLocked(receiver);
+                } else {
+                    Slog.v(TAG, "getSupportedLocales(): no service for " + userId);
+                    receiver.send(STATUS_SYNC_CALL_FAIL, null);
+                }
+            }
+        }
+
+        @Override
+        public void onSessionCreated(TranslationSpec sourceSpec, TranslationSpec destSpec,
+                int sessionId, IResultReceiver receiver, int userId) throws RemoteException {
+            synchronized (mLock) {
+                final TranslationManagerServiceImpl service = getServiceForUserLocked(userId);
+                if (service != null) {
+                    service.onSessionCreatedLocked(sourceSpec, destSpec, sessionId, receiver);
+                } else {
+                    Slog.v(TAG, "onSessionCreated(): no service for " + userId);
+                    receiver.send(STATUS_SYNC_CALL_FAIL, null);
+                }
+            }
+        }
+    }
+
+    @Override // from SystemService
+    public void onStart() {
+        publishBinderService(TRANSLATION_MANAGER_SERVICE,
+                new TranslationManagerService.TranslationManagerServiceStub());
+    }
+}
diff --git a/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java b/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java
new file mode 100644
index 0000000..b1f6f80
--- /dev/null
+++ b/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java
@@ -0,0 +1,125 @@
+/*
+ * 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.STATUS_SYNC_CALL_SUCCESS;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.os.RemoteException;
+import android.service.translation.TranslationServiceInfo;
+import android.util.Slog;
+import android.view.translation.TranslationSpec;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.os.IResultReceiver;
+import com.android.internal.util.SyncResultReceiver;
+import com.android.server.infra.AbstractPerUserSystemService;
+
+import java.util.ArrayList;
+
+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;
+
+    protected TranslationManagerServiceImpl(
+            @NonNull TranslationManagerService master,
+            @NonNull Object lock, int userId, boolean disabled) {
+        super(master, lock, userId);
+        updateRemoteServiceLocked();
+    }
+
+    @GuardedBy("mLock")
+    @Override // from PerUserSystemService
+    protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent)
+            throws PackageManager.NameNotFoundException {
+        final TranslationServiceInfo info = new TranslationServiceInfo(getContext(),
+                serviceComponent, isTemporaryServiceSetLocked(), mUserId);
+        mRemoteTranslationServiceInfo = info.getServiceInfo();
+        return info.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);
+        }
+        return mRemoteTranslationService;
+    }
+
+    @GuardedBy("mLock")
+    void getSupportedLocalesLocked(@NonNull IResultReceiver resultReceiver) {
+        // TODO: implement this
+        try {
+            resultReceiver.send(STATUS_SYNC_CALL_SUCCESS,
+                    SyncResultReceiver.bundleFor(new ArrayList<>()));
+        } catch (RemoteException e) {
+            Slog.w(TAG, "RemoteException returning supported locales: " + e);
+        }
+    }
+
+    @GuardedBy("mLock")
+    void onSessionCreatedLocked(@NonNull TranslationSpec sourceSpec,
+            @NonNull TranslationSpec destSpec, int sessionId, IResultReceiver resultReceiver) {
+        final RemoteTranslationService remoteService = ensureRemoteServiceLocked();
+        if (remoteService != null) {
+            remoteService.onSessionCreated(sourceSpec, destSpec, sessionId, resultReceiver);
+        }
+    }
+}