/*
|
* Copyright (C) 2015 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.pm;
|
|
import android.annotation.AnyThread;
|
import android.annotation.WorkerThread;
|
import android.app.IInstantAppResolver;
|
import android.app.InstantAppResolverService;
|
import android.content.ComponentName;
|
import android.content.Context;
|
import android.content.Intent;
|
import android.content.ServiceConnection;
|
import android.content.pm.InstantAppResolveInfo;
|
import android.os.Binder;
|
import android.os.Build;
|
import android.os.Bundle;
|
import android.os.Handler;
|
import android.os.IBinder;
|
import android.os.IBinder.DeathRecipient;
|
import android.os.IRemoteCallback;
|
import android.os.RemoteException;
|
import android.os.SystemClock;
|
import android.os.UserHandle;
|
import android.util.Slog;
|
import android.util.TimedRemoteCaller;
|
|
import com.android.internal.annotations.GuardedBy;
|
import com.android.internal.os.BackgroundThread;
|
|
import java.util.ArrayList;
|
import java.util.List;
|
import java.util.NoSuchElementException;
|
import java.util.concurrent.TimeoutException;
|
|
/**
|
* Represents a remote instant app resolver. It is responsible for binding to the remote
|
* service and handling all interactions in a timely manner.
|
* @hide
|
*/
|
final class InstantAppResolverConnection implements DeathRecipient {
|
private static final String TAG = "PackageManager";
|
// This is running in a critical section and the timeout must be sufficiently low
|
private static final long BIND_SERVICE_TIMEOUT_MS =
|
Build.IS_ENG ? 500 : 300;
|
private static final long CALL_SERVICE_TIMEOUT_MS =
|
Build.IS_ENG ? 200 : 100;
|
private static final boolean DEBUG_INSTANT = Build.IS_DEBUGGABLE;
|
|
private final Object mLock = new Object();
|
private final GetInstantAppResolveInfoCaller mGetInstantAppResolveInfoCaller =
|
new GetInstantAppResolveInfoCaller();
|
private final ServiceConnection mServiceConnection = new MyServiceConnection();
|
private final Context mContext;
|
/** Intent used to bind to the service */
|
private final Intent mIntent;
|
|
private static final int STATE_IDLE = 0; // no bind operation is ongoing
|
private static final int STATE_BINDING = 1; // someone is binding and waiting
|
private static final int STATE_PENDING = 2; // a bind is pending, but the caller is not waiting
|
private final Handler mBgHandler;
|
|
@GuardedBy("mLock")
|
private int mBindState = STATE_IDLE;
|
@GuardedBy("mLock")
|
private IInstantAppResolver mRemoteInstance;
|
|
public InstantAppResolverConnection(
|
Context context, ComponentName componentName, String action) {
|
mContext = context;
|
mIntent = new Intent(action).setComponent(componentName);
|
mBgHandler = BackgroundThread.getHandler();
|
}
|
|
public List<InstantAppResolveInfo> getInstantAppResolveInfoList(Intent sanitizedIntent,
|
int[] hashPrefix, int userId, String token) throws ConnectionException {
|
throwIfCalledOnMainThread();
|
IInstantAppResolver target = null;
|
try {
|
try {
|
target = getRemoteInstanceLazy(token);
|
} catch (TimeoutException e) {
|
throw new ConnectionException(ConnectionException.FAILURE_BIND);
|
} catch (InterruptedException e) {
|
throw new ConnectionException(ConnectionException.FAILURE_INTERRUPTED);
|
}
|
try {
|
return mGetInstantAppResolveInfoCaller
|
.getInstantAppResolveInfoList(target, sanitizedIntent, hashPrefix, userId,
|
token);
|
} catch (TimeoutException e) {
|
throw new ConnectionException(ConnectionException.FAILURE_CALL);
|
} catch (RemoteException ignore) {
|
}
|
} finally {
|
synchronized (mLock) {
|
mLock.notifyAll();
|
}
|
}
|
return null;
|
}
|
|
public void getInstantAppIntentFilterList(Intent sanitizedIntent, int[] hashPrefix, int userId,
|
String token, PhaseTwoCallback callback, Handler callbackHandler, final long startTime)
|
throws ConnectionException {
|
final IRemoteCallback remoteCallback = new IRemoteCallback.Stub() {
|
@Override
|
public void sendResult(Bundle data) throws RemoteException {
|
final ArrayList<InstantAppResolveInfo> resolveList =
|
data.getParcelableArrayList(
|
InstantAppResolverService.EXTRA_RESOLVE_INFO);
|
callbackHandler.post(() -> callback.onPhaseTwoResolved(resolveList, startTime));
|
}
|
};
|
try {
|
getRemoteInstanceLazy(token)
|
.getInstantAppIntentFilterList(sanitizedIntent, hashPrefix, userId, token,
|
remoteCallback);
|
} catch (TimeoutException e) {
|
throw new ConnectionException(ConnectionException.FAILURE_BIND);
|
} catch (InterruptedException e) {
|
throw new ConnectionException(ConnectionException.FAILURE_INTERRUPTED);
|
} catch (RemoteException ignore) {
|
}
|
}
|
|
@WorkerThread
|
private IInstantAppResolver getRemoteInstanceLazy(String token)
|
throws ConnectionException, TimeoutException, InterruptedException {
|
long binderToken = Binder.clearCallingIdentity();
|
try {
|
return bind(token);
|
} finally {
|
Binder.restoreCallingIdentity(binderToken);
|
}
|
}
|
|
@GuardedBy("mLock")
|
private void waitForBindLocked(String token) throws TimeoutException, InterruptedException {
|
final long startMillis = SystemClock.uptimeMillis();
|
while (mBindState != STATE_IDLE) {
|
if (mRemoteInstance != null) {
|
break;
|
}
|
final long elapsedMillis = SystemClock.uptimeMillis() - startMillis;
|
final long remainingMillis = BIND_SERVICE_TIMEOUT_MS - elapsedMillis;
|
if (remainingMillis <= 0) {
|
throw new TimeoutException("[" + token + "] Didn't bind to resolver in time!");
|
}
|
mLock.wait(remainingMillis);
|
}
|
}
|
|
@WorkerThread
|
private IInstantAppResolver bind(String token)
|
throws ConnectionException, TimeoutException, InterruptedException {
|
boolean doUnbind = false;
|
synchronized (mLock) {
|
if (mRemoteInstance != null) {
|
return mRemoteInstance;
|
}
|
|
if (mBindState == STATE_PENDING) {
|
// there is a pending bind, let's see if we can use it.
|
if (DEBUG_INSTANT) {
|
Slog.i(TAG, "[" + token + "] Previous bind timed out; waiting for connection");
|
}
|
try {
|
waitForBindLocked(token);
|
if (mRemoteInstance != null) {
|
return mRemoteInstance;
|
}
|
} catch (TimeoutException e) {
|
// nope, we might have to try a rebind.
|
doUnbind = true;
|
}
|
}
|
|
if (mBindState == STATE_BINDING) {
|
// someone was binding when we called bind(), or they raced ahead while we were
|
// waiting in the PENDING case; wait for their result instead. Last chance!
|
if (DEBUG_INSTANT) {
|
Slog.i(TAG, "[" + token + "] Another thread is binding; waiting for connection");
|
}
|
waitForBindLocked(token);
|
// if the other thread's bindService() returned false, we could still have null.
|
if (mRemoteInstance != null) {
|
return mRemoteInstance;
|
}
|
throw new ConnectionException(ConnectionException.FAILURE_BIND);
|
}
|
mBindState = STATE_BINDING; // our time to shine! :)
|
}
|
|
// only one thread can be here at a time (the one that set STATE_BINDING)
|
boolean wasBound = false;
|
IInstantAppResolver instance = null;
|
try {
|
if (doUnbind) {
|
if (DEBUG_INSTANT) {
|
Slog.i(TAG, "[" + token + "] Previous connection never established; rebinding");
|
}
|
mContext.unbindService(mServiceConnection);
|
}
|
if (DEBUG_INSTANT) {
|
Slog.v(TAG, "[" + token + "] Binding to instant app resolver");
|
}
|
final int flags = Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE;
|
wasBound = mContext
|
.bindServiceAsUser(mIntent, mServiceConnection, flags, UserHandle.SYSTEM);
|
if (wasBound) {
|
synchronized (mLock) {
|
waitForBindLocked(token);
|
instance = mRemoteInstance;
|
return instance;
|
}
|
} else {
|
Slog.w(TAG, "[" + token + "] Failed to bind to: " + mIntent);
|
throw new ConnectionException(ConnectionException.FAILURE_BIND);
|
}
|
} finally {
|
synchronized (mLock) {
|
if (wasBound && instance == null) {
|
mBindState = STATE_PENDING;
|
} else {
|
mBindState = STATE_IDLE;
|
}
|
mLock.notifyAll();
|
}
|
}
|
}
|
|
private void throwIfCalledOnMainThread() {
|
if (Thread.currentThread() == mContext.getMainLooper().getThread()) {
|
throw new RuntimeException("Cannot invoke on the main thread");
|
}
|
}
|
|
@AnyThread
|
void optimisticBind() {
|
mBgHandler.post(() -> {
|
try {
|
if (bind("Optimistic Bind") != null && DEBUG_INSTANT) {
|
Slog.i(TAG, "Optimistic bind succeeded.");
|
}
|
} catch (ConnectionException | TimeoutException | InterruptedException e) {
|
Slog.e(TAG, "Optimistic bind failed.", e);
|
}
|
});
|
}
|
|
@Override
|
public void binderDied() {
|
if (DEBUG_INSTANT) {
|
Slog.d(TAG, "Binder to instant app resolver died");
|
}
|
synchronized (mLock) {
|
handleBinderDiedLocked();
|
}
|
optimisticBind();
|
}
|
|
@GuardedBy("mLock")
|
private void handleBinderDiedLocked() {
|
if (mRemoteInstance != null) {
|
try {
|
mRemoteInstance.asBinder().unlinkToDeath(this, 0 /*flags*/);
|
} catch (NoSuchElementException ignore) { }
|
}
|
mRemoteInstance = null;
|
}
|
|
/**
|
* Asynchronous callback when results come back from ephemeral resolution phase two.
|
*/
|
public abstract static class PhaseTwoCallback {
|
abstract void onPhaseTwoResolved(
|
List<InstantAppResolveInfo> instantAppResolveInfoList, long startTime);
|
}
|
|
public static class ConnectionException extends Exception {
|
public static final int FAILURE_BIND = 1;
|
public static final int FAILURE_CALL = 2;
|
public static final int FAILURE_INTERRUPTED = 3;
|
|
public final int failure;
|
public ConnectionException(int _failure) {
|
failure = _failure;
|
}
|
}
|
|
private final class MyServiceConnection implements ServiceConnection {
|
@Override
|
public void onServiceConnected(ComponentName name, IBinder service) {
|
if (DEBUG_INSTANT) {
|
Slog.d(TAG, "Connected to instant app resolver");
|
}
|
synchronized (mLock) {
|
mRemoteInstance = IInstantAppResolver.Stub.asInterface(service);
|
if (mBindState == STATE_PENDING) {
|
mBindState = STATE_IDLE;
|
}
|
try {
|
service.linkToDeath(InstantAppResolverConnection.this, 0 /*flags*/);
|
} catch (RemoteException e) {
|
handleBinderDiedLocked();
|
}
|
mLock.notifyAll();
|
}
|
}
|
|
@Override
|
public void onServiceDisconnected(ComponentName name) {
|
if (DEBUG_INSTANT) {
|
Slog.d(TAG, "Disconnected from instant app resolver");
|
}
|
synchronized (mLock) {
|
handleBinderDiedLocked();
|
}
|
}
|
}
|
|
private static final class GetInstantAppResolveInfoCaller
|
extends TimedRemoteCaller<List<InstantAppResolveInfo>> {
|
private final IRemoteCallback mCallback;
|
|
public GetInstantAppResolveInfoCaller() {
|
super(CALL_SERVICE_TIMEOUT_MS);
|
mCallback = new IRemoteCallback.Stub() {
|
@Override
|
public void sendResult(Bundle data) throws RemoteException {
|
final ArrayList<InstantAppResolveInfo> resolveList =
|
data.getParcelableArrayList(
|
InstantAppResolverService.EXTRA_RESOLVE_INFO);
|
int sequence =
|
data.getInt(InstantAppResolverService.EXTRA_SEQUENCE, -1);
|
onRemoteMethodResult(resolveList, sequence);
|
}
|
};
|
}
|
|
public List<InstantAppResolveInfo> getInstantAppResolveInfoList(
|
IInstantAppResolver target, Intent sanitizedIntent, int[] hashPrefix, int userId,
|
String token) throws RemoteException, TimeoutException {
|
final int sequence = onBeforeRemoteCall();
|
target.getInstantAppResolveInfoList(sanitizedIntent, hashPrefix, userId, token,
|
sequence, mCallback);
|
return getResultTimed(sequence);
|
}
|
}
|
}
|