/*
 * Copyright (C) 2017 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.systemui;

import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.graphics.Rect;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.PatternMatcher;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
import android.view.SurfaceControl;

import com.android.systemui.OverviewProxyService.OverviewProxyListener;
import com.android.systemui.recents.events.EventBus;
import com.android.systemui.recents.events.activity.DockedFirstAnimationFrameEvent;
import com.android.systemui.recents.misc.SystemServicesProxy;
import com.android.systemui.shared.recents.IOverviewProxy;
import com.android.systemui.shared.recents.ISystemUiProxy;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.GraphicBufferCompat;
import com.android.systemui.stackdivider.Divider;
import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.statusbar.policy.CallbackController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_DISABLE_SWIPE_UP;
import static com.android.systemui.shared.system.NavigationBarCompat.InteractionType;

Class to send information from overview to launcher with a binder.
/** * Class to send information from overview to launcher with a binder. */
public class OverviewProxyService implements CallbackController<OverviewProxyListener>, Dumpable { private static final String ACTION_QUICKSTEP = "android.intent.action.QUICKSTEP_SERVICE"; public static final String TAG_OPS = "OverviewProxyService"; public static final boolean DEBUG_OVERVIEW_PROXY = false; private static final long BACKOFF_MILLIS = 5000; private static final long DEFERRED_CALLBACK_MILLIS = 5000; private final Context mContext; private final Handler mHandler; private final Runnable mConnectionRunnable = this::internalConnectToCurrentUser; private final ComponentName mRecentsComponentName; private final DeviceProvisionedController mDeviceProvisionedController = Dependency.get(DeviceProvisionedController.class); private final List<OverviewProxyListener> mConnectionCallbacks = new ArrayList<>(); private final Intent mQuickStepIntent; private IOverviewProxy mOverviewProxy; private int mConnectionBackoffAttempts; private @InteractionType int mInteractionFlags; private boolean mIsEnabled; private ISystemUiProxy mSysUiProxy = new ISystemUiProxy.Stub() { public GraphicBufferCompat screenshot(Rect sourceCrop, int width, int height, int minLayer, int maxLayer, boolean useIdentityTransform, int rotation) { long token = Binder.clearCallingIdentity(); try { return new GraphicBufferCompat(SurfaceControl.screenshotToBuffer(sourceCrop, width, height, minLayer, maxLayer, useIdentityTransform, rotation)); } finally { Binder.restoreCallingIdentity(token); } } public void startScreenPinning(int taskId) { long token = Binder.clearCallingIdentity(); try { mHandler.post(() -> { StatusBar statusBar = ((SystemUIApplication) mContext).getComponent( StatusBar.class); if (statusBar != null) { statusBar.showScreenPinningRequest(taskId, false /* allowCancel */); } }); } finally { Binder.restoreCallingIdentity(token); } } public void onSplitScreenInvoked() { long token = Binder.clearCallingIdentity(); try { EventBus.getDefault().post(new DockedFirstAnimationFrameEvent()); } finally { Binder.restoreCallingIdentity(token); } } public void onOverviewShown(boolean fromHome) { long token = Binder.clearCallingIdentity(); try { mHandler.post(() -> { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).onOverviewShown(fromHome); } }); } finally { Binder.restoreCallingIdentity(token); } } public void setInteractionState(@InteractionType int flags) { long token = Binder.clearCallingIdentity(); try { if (mInteractionFlags != flags) { mInteractionFlags = flags; mHandler.post(() -> { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).onInteractionFlagsChanged(flags); } }); } } finally { Prefs.putInt(mContext, Prefs.Key.QUICK_STEP_INTERACTION_FLAGS, mInteractionFlags); Binder.restoreCallingIdentity(token); } } public Rect getNonMinimizedSplitScreenSecondaryBounds() { long token = Binder.clearCallingIdentity(); try { Divider divider = ((SystemUIApplication) mContext).getComponent(Divider.class); if (divider != null) { return divider.getView().getNonMinimizedSplitScreenSecondaryBounds(); } return null; } finally { Binder.restoreCallingIdentity(token); } } public void setBackButtonAlpha(float alpha, boolean animate) { long token = Binder.clearCallingIdentity(); try { mHandler.post(() -> { notifyBackButtonAlphaChanged(alpha, animate); }); } finally { Binder.restoreCallingIdentity(token); } } }; private final Runnable mDeferredConnectionCallback = () -> { Log.w(TAG_OPS, "Binder supposed established connection but actual connection to service " + "timed out, trying again"); internalConnectToCurrentUser(); }; private final BroadcastReceiver mLauncherStateChangedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { updateEnabledState(); // When launcher service is disabled, reset interaction flags because it is inactive if (!isEnabled()) { mInteractionFlags = 0; Prefs.remove(mContext, Prefs.Key.QUICK_STEP_INTERACTION_FLAGS); } // Reconnect immediately, instead of waiting for resume to arrive. startConnectionToCurrentUser(); } }; private final ServiceConnection mOverviewServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { mHandler.removeCallbacks(mDeferredConnectionCallback); mConnectionBackoffAttempts = 0; mOverviewProxy = IOverviewProxy.Stub.asInterface(service); // Listen for launcher's death try { service.linkToDeath(mOverviewServiceDeathRcpt, 0); } catch (RemoteException e) { Log.e(TAG_OPS, "Lost connection to launcher service", e); } try { mOverviewProxy.onBind(mSysUiProxy); } catch (RemoteException e) { Log.e(TAG_OPS, "Failed to call onBind()", e); } notifyConnectionChanged(); } @Override public void onNullBinding(ComponentName name) { Log.w(TAG_OPS, "Null binding of '" + name + "', try reconnecting"); internalConnectToCurrentUser(); } @Override public void onBindingDied(ComponentName name) { Log.w(TAG_OPS, "Binding died of '" + name + "', try reconnecting"); internalConnectToCurrentUser(); } @Override public void onServiceDisconnected(ComponentName name) { // Do nothing } }; private final DeviceProvisionedListener mDeviceProvisionedCallback = new DeviceProvisionedListener() { @Override public void onUserSetupChanged() { if (mDeviceProvisionedController.isCurrentUserSetup()) { internalConnectToCurrentUser(); } } @Override public void onUserSwitched() { mConnectionBackoffAttempts = 0; internalConnectToCurrentUser(); } }; // This is the death handler for the binder from the launcher service private final IBinder.DeathRecipient mOverviewServiceDeathRcpt = this::startConnectionToCurrentUser; public OverviewProxyService(Context context) { mContext = context; mHandler = new Handler(); mConnectionBackoffAttempts = 0; mRecentsComponentName = ComponentName.unflattenFromString(context.getString( com.android.internal.R.string.config_recentsComponentName)); mQuickStepIntent = new Intent(ACTION_QUICKSTEP) .setPackage(mRecentsComponentName.getPackageName()); mInteractionFlags = Prefs.getInt(mContext, Prefs.Key.QUICK_STEP_INTERACTION_FLAGS, 0); // Listen for the package update changes. if (SystemServicesProxy.getInstance(context) .isSystemUser(mDeviceProvisionedController.getCurrentUser())) { updateEnabledState(); mDeviceProvisionedController.addCallback(mDeviceProvisionedCallback); IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); filter.addDataScheme("package"); filter.addDataSchemeSpecificPart(mRecentsComponentName.getPackageName(), PatternMatcher.PATTERN_LITERAL); filter.addAction(Intent.ACTION_PACKAGE_CHANGED); mContext.registerReceiver(mLauncherStateChangedReceiver, filter); } } public void startConnectionToCurrentUser() { if (mHandler.getLooper() != Looper.myLooper()) { mHandler.post(mConnectionRunnable); } else { internalConnectToCurrentUser(); } } private void internalConnectToCurrentUser() { disconnectFromLauncherService(); // If user has not setup yet or already connected, do not try to connect if (!mDeviceProvisionedController.isCurrentUserSetup() || !isEnabled()) { Log.v(TAG_OPS, "Cannot attempt connection, is setup " + mDeviceProvisionedController.isCurrentUserSetup() + ", is enabled " + isEnabled()); return; } mHandler.removeCallbacks(mConnectionRunnable); Intent launcherServiceIntent = new Intent(ACTION_QUICKSTEP) .setPackage(mRecentsComponentName.getPackageName()); boolean bound = false; try { bound = mContext.bindServiceAsUser(launcherServiceIntent, mOverviewServiceConnection, Context.BIND_AUTO_CREATE, UserHandle.of(mDeviceProvisionedController.getCurrentUser())); } catch (SecurityException e) { Log.e(TAG_OPS, "Unable to bind because of security error", e); } if (bound) { // Ensure that connection has been established even if it thinks it is bound mHandler.postDelayed(mDeferredConnectionCallback, DEFERRED_CALLBACK_MILLIS); } else { // Retry after exponential backoff timeout final long timeoutMs = (long) Math.scalb(BACKOFF_MILLIS, mConnectionBackoffAttempts); mHandler.postDelayed(mConnectionRunnable, timeoutMs); mConnectionBackoffAttempts++; Log.w(TAG_OPS, "Failed to connect on attempt " + mConnectionBackoffAttempts + " will try again in " + timeoutMs + "ms"); } } @Override public void addCallback(OverviewProxyListener listener) { mConnectionCallbacks.add(listener); listener.onConnectionChanged(mOverviewProxy != null); listener.onInteractionFlagsChanged(mInteractionFlags); } @Override public void removeCallback(OverviewProxyListener listener) { mConnectionCallbacks.remove(listener); } public boolean shouldShowSwipeUpUI() { return isEnabled() && ((mInteractionFlags & FLAG_DISABLE_SWIPE_UP) == 0); } public boolean isEnabled() { return mIsEnabled; } public IOverviewProxy getProxy() { return mOverviewProxy; } public int getInteractionFlags() { return mInteractionFlags; } private void disconnectFromLauncherService() { if (mOverviewProxy != null) { mOverviewProxy.asBinder().unlinkToDeath(mOverviewServiceDeathRcpt, 0); mContext.unbindService(mOverviewServiceConnection); mOverviewProxy = null; notifyBackButtonAlphaChanged(1f, false /* animate */); notifyConnectionChanged(); } } private void notifyBackButtonAlphaChanged(float alpha, boolean animate) { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).onBackButtonAlphaChanged(alpha, animate); } } private void notifyConnectionChanged() { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).onConnectionChanged(mOverviewProxy != null); } } public void notifyQuickStepStarted() { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).onQuickStepStarted(); } } public void notifyQuickScrubStarted() { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).onQuickScrubStarted(); } } private void updateEnabledState() { mIsEnabled = mContext.getPackageManager().resolveServiceAsUser(mQuickStepIntent, MATCH_DIRECT_BOOT_UNAWARE, ActivityManagerWrapper.getInstance().getCurrentUserId()) != null; } @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println(TAG_OPS + " state:"); pw.print(" mConnectionBackoffAttempts="); pw.println(mConnectionBackoffAttempts); pw.print(" isCurrentUserSetup="); pw.println(mDeviceProvisionedController .isCurrentUserSetup()); pw.print(" isConnected="); pw.println(mOverviewProxy != null); pw.print(" mRecentsComponentName="); pw.println(mRecentsComponentName); pw.print(" mIsEnabled="); pw.println(isEnabled()); pw.print(" mInteractionFlags="); pw.println(mInteractionFlags); pw.print(" mQuickStepIntent="); pw.println(mQuickStepIntent); } public interface OverviewProxyListener { default void onConnectionChanged(boolean isConnected) {} default void onQuickStepStarted() {} default void onInteractionFlagsChanged(@InteractionType int flags) {} default void onOverviewShown(boolean fromHome) {} default void onQuickScrubStarted() {} default void onBackButtonAlphaChanged(float alpha, boolean animate) {} } }