| /* |
| * Copyright (C) 2021 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.wm.shell.transition; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.ActivityTaskManager; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.util.ArrayMap; |
| import android.util.Log; |
| import android.util.Pair; |
| import android.util.Slog; |
| import android.view.SurfaceControl; |
| import android.window.IRemoteTransition; |
| import android.window.IRemoteTransitionFinishedCallback; |
| import android.window.RemoteTransition; |
| import android.window.TransitionFilter; |
| import android.window.TransitionInfo; |
| import android.window.TransitionRequestInfo; |
| import android.window.WindowContainerTransaction; |
| |
| import androidx.annotation.BinderThread; |
| |
| import com.android.internal.protolog.common.ProtoLog; |
| import com.android.wm.shell.common.ShellExecutor; |
| import com.android.wm.shell.protolog.ShellProtoLogGroup; |
| |
| import java.util.ArrayList; |
| |
| /** |
| * Handler that deals with RemoteTransitions. It will only request to handle a transition |
| * if the request includes a specific remote. |
| */ |
| public class RemoteTransitionHandler implements Transitions.TransitionHandler { |
| private static final String TAG = "RemoteTransitionHandler"; |
| |
| private final ShellExecutor mMainExecutor; |
| |
| /** Includes remotes explicitly requested by, eg, ActivityOptions */ |
| private final ArrayMap<IBinder, RemoteTransition> mRequestedRemotes = new ArrayMap<>(); |
| |
| /** Ordered by specificity. Last filters will be checked first */ |
| private final ArrayList<Pair<TransitionFilter, RemoteTransition>> mFilters = |
| new ArrayList<>(); |
| |
| private final ArrayMap<IBinder, RemoteDeathHandler> mDeathHandlers = new ArrayMap<>(); |
| |
| RemoteTransitionHandler(@NonNull ShellExecutor mainExecutor) { |
| mMainExecutor = mainExecutor; |
| } |
| |
| void addFiltered(TransitionFilter filter, RemoteTransition remote) { |
| handleDeath(remote.asBinder(), null /* finishCallback */); |
| mFilters.add(new Pair<>(filter, remote)); |
| } |
| |
| void removeFiltered(RemoteTransition remote) { |
| boolean removed = false; |
| for (int i = mFilters.size() - 1; i >= 0; --i) { |
| if (mFilters.get(i).second.asBinder().equals(remote.asBinder())) { |
| mFilters.remove(i); |
| removed = true; |
| } |
| } |
| if (removed) { |
| unhandleDeath(remote.asBinder(), null /* finishCallback */); |
| } |
| } |
| |
| @Override |
| public void onTransitionMerged(@NonNull IBinder transition) { |
| mRequestedRemotes.remove(transition); |
| } |
| |
| @Override |
| public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, |
| @NonNull SurfaceControl.Transaction startTransaction, |
| @NonNull SurfaceControl.Transaction finishTransaction, |
| @NonNull Transitions.TransitionFinishCallback finishCallback) { |
| RemoteTransition pendingRemote = mRequestedRemotes.get(transition); |
| if (pendingRemote == null) { |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition %s doesn't have " |
| + "explicit remote, search filters for match for %s", transition, info); |
| // If no explicit remote, search filters until one matches |
| for (int i = mFilters.size() - 1; i >= 0; --i) { |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Checking filter %s", |
| mFilters.get(i)); |
| if (mFilters.get(i).first.matches(info)) { |
| Slog.d(TAG, "Found filter" + mFilters.get(i)); |
| pendingRemote = mFilters.get(i).second; |
| // Add to requested list so that it can be found for merge requests. |
| mRequestedRemotes.put(transition, pendingRemote); |
| break; |
| } |
| } |
| } |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Delegate animation for %s to %s", |
| transition, pendingRemote); |
| |
| if (pendingRemote == null) return false; |
| |
| final RemoteTransition remote = pendingRemote; |
| IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() { |
| @Override |
| public void onTransitionFinished(WindowContainerTransaction wct, |
| SurfaceControl.Transaction sct) { |
| unhandleDeath(remote.asBinder(), finishCallback); |
| mMainExecutor.execute(() -> { |
| if (sct != null) { |
| finishTransaction.merge(sct); |
| } |
| mRequestedRemotes.remove(transition); |
| finishCallback.onTransitionFinished(wct, null /* wctCB */); |
| }); |
| } |
| }; |
| try { |
| handleDeath(remote.asBinder(), finishCallback); |
| try { |
| ActivityTaskManager.getService().setRunningRemoteTransitionDelegate( |
| remote.getAppThread()); |
| } catch (SecurityException e) { |
| Log.e(Transitions.TAG, "Unable to boost animation thread. This should only happen" |
| + " during unit tests"); |
| } |
| remote.getRemoteTransition().startAnimation(transition, info, startTransaction, cb); |
| } catch (RemoteException e) { |
| Log.e(Transitions.TAG, "Error running remote transition.", e); |
| unhandleDeath(remote.asBinder(), finishCallback); |
| mRequestedRemotes.remove(transition); |
| mMainExecutor.execute( |
| () -> finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */)); |
| } |
| return true; |
| } |
| |
| @Override |
| public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, |
| @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, |
| @NonNull Transitions.TransitionFinishCallback finishCallback) { |
| final IRemoteTransition remote = mRequestedRemotes.get(mergeTarget).getRemoteTransition(); |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Attempt merge %s into %s", |
| transition, remote); |
| if (remote == null) return; |
| |
| IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() { |
| @Override |
| public void onTransitionFinished(WindowContainerTransaction wct, |
| SurfaceControl.Transaction sct) { |
| mMainExecutor.execute(() -> { |
| if (!mRequestedRemotes.containsKey(mergeTarget)) { |
| Log.e(TAG, "Merged transition finished after it's mergeTarget (the " |
| + "transition it was supposed to merge into). This usually means " |
| + "that the mergeTarget's RemoteTransition impl erroneously " |
| + "accepted/ran the merge request after finishing the mergeTarget"); |
| } |
| finishCallback.onTransitionFinished(wct, null /* wctCB */); |
| }); |
| } |
| }; |
| try { |
| remote.mergeAnimation(transition, info, t, mergeTarget, cb); |
| } catch (RemoteException e) { |
| Log.e(Transitions.TAG, "Error attempting to merge remote transition.", e); |
| } |
| } |
| |
| @Override |
| @Nullable |
| public WindowContainerTransaction handleRequest(@NonNull IBinder transition, |
| @Nullable TransitionRequestInfo request) { |
| RemoteTransition remote = request.getRemoteTransition(); |
| if (remote == null) return null; |
| mRequestedRemotes.put(transition, remote); |
| ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "RemoteTransition directly requested" |
| + " for %s: %s", transition, remote); |
| return new WindowContainerTransaction(); |
| } |
| |
| private void handleDeath(@NonNull IBinder remote, |
| @Nullable Transitions.TransitionFinishCallback finishCallback) { |
| synchronized (mDeathHandlers) { |
| RemoteDeathHandler deathHandler = mDeathHandlers.get(remote); |
| if (deathHandler == null) { |
| deathHandler = new RemoteDeathHandler(remote); |
| try { |
| remote.linkToDeath(deathHandler, 0 /* flags */); |
| } catch (RemoteException e) { |
| Slog.e(TAG, "Failed to link to death"); |
| return; |
| } |
| mDeathHandlers.put(remote, deathHandler); |
| } |
| deathHandler.addUser(finishCallback); |
| } |
| } |
| |
| private void unhandleDeath(@NonNull IBinder remote, |
| @Nullable Transitions.TransitionFinishCallback finishCallback) { |
| synchronized (mDeathHandlers) { |
| RemoteDeathHandler deathHandler = mDeathHandlers.get(remote); |
| if (deathHandler == null) return; |
| deathHandler.removeUser(finishCallback); |
| if (deathHandler.getUserCount() == 0) { |
| if (!deathHandler.mPendingFinishCallbacks.isEmpty()) { |
| throw new IllegalStateException("Unhandling death for binder that still has" |
| + " pending finishCallback(s)."); |
| } |
| remote.unlinkToDeath(deathHandler, 0 /* flags */); |
| mDeathHandlers.remove(remote); |
| } |
| } |
| } |
| |
| /** NOTE: binder deaths can alter the filter order */ |
| private class RemoteDeathHandler implements IBinder.DeathRecipient { |
| private final IBinder mRemote; |
| private final ArrayList<Transitions.TransitionFinishCallback> mPendingFinishCallbacks = |
| new ArrayList<>(); |
| private int mUsers = 0; |
| |
| RemoteDeathHandler(IBinder remote) { |
| mRemote = remote; |
| } |
| |
| void addUser(@Nullable Transitions.TransitionFinishCallback finishCallback) { |
| if (finishCallback != null) { |
| mPendingFinishCallbacks.add(finishCallback); |
| } |
| ++mUsers; |
| } |
| |
| void removeUser(@Nullable Transitions.TransitionFinishCallback finishCallback) { |
| if (finishCallback != null) { |
| mPendingFinishCallbacks.remove(finishCallback); |
| } |
| --mUsers; |
| } |
| |
| int getUserCount() { |
| return mUsers; |
| } |
| |
| @Override |
| @BinderThread |
| public void binderDied() { |
| mMainExecutor.execute(() -> { |
| for (int i = mFilters.size() - 1; i >= 0; --i) { |
| if (mRemote.equals(mFilters.get(i).second.asBinder())) { |
| mFilters.remove(i); |
| } |
| } |
| for (int i = mRequestedRemotes.size() - 1; i >= 0; --i) { |
| if (mRemote.equals(mRequestedRemotes.valueAt(i).asBinder())) { |
| mRequestedRemotes.removeAt(i); |
| } |
| } |
| for (int i = mPendingFinishCallbacks.size() - 1; i >= 0; --i) { |
| mPendingFinishCallbacks.get(i).onTransitionFinished( |
| null /* wct */, null /* wctCB */); |
| } |
| mPendingFinishCallbacks.clear(); |
| }); |
| } |
| } |
| } |