1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.devicelockcontroller.storage;
18 
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.ServiceConnection;
23 import android.os.IBinder;
24 
25 import androidx.annotation.GuardedBy;
26 import androidx.annotation.Nullable;
27 import androidx.annotation.VisibleForTesting;
28 
29 import com.android.devicelockcontroller.util.LogUtil;
30 
31 import com.google.common.util.concurrent.ListenableFuture;
32 import com.google.common.util.concurrent.ListeningExecutorService;
33 
34 import java.util.Objects;
35 import java.util.concurrent.Callable;
36 
37 /**
38  * This class implements a client what automatically binds to a service and allows
39  * asynchronous calls.
40  * Subclasses can invoke the "call" method, that is responsible for binding and returning a
41  * listenable future.
42  */
43 abstract class DlcClient {
44     private static final String TAG = "DlcClient";
45     private final Object mLock = new Object();
46     @GuardedBy("mLock")
47     private IBinder mDlcService;
48 
49     @GuardedBy("mLock")
50     private ServiceConnection mServiceConnection;
51 
52     private Context mContext;
53 
54     private final ComponentName mComponentName;
55     private final ListeningExecutorService mListeningExecutorService;
56 
57     private class DlcServiceConnection implements ServiceConnection {
58         @Override
onServiceConnected(ComponentName name, IBinder service)59         public void onServiceConnected(ComponentName name, IBinder service) {
60             synchronized (mLock) {
61                 mDlcService = service;
62                 mLock.notifyAll();
63             }
64         }
65 
66         @Override
onServiceDisconnected(ComponentName name)67         public void onServiceDisconnected(ComponentName name) {
68             // Binding still valid, unbind anyway so we can bind again as needed.
69             unbind();
70         }
71 
72         @Override
onBindingDied(ComponentName name)73         public void onBindingDied(ComponentName name) {
74             unbind();
75         }
76     }
77 
DlcClient(Context context, ComponentName componentName, @Nullable ListeningExecutorService executorService)78     DlcClient(Context context, ComponentName componentName,
79             @Nullable ListeningExecutorService executorService) {
80         mContext = context;
81         mComponentName = componentName;
82         mListeningExecutorService = executorService;
83     }
84 
getService()85     IBinder getService() {
86         synchronized (mLock) {
87             return Objects.requireNonNull(mDlcService);
88         }
89     }
90 
91     @VisibleForTesting
setService(IBinder service)92     public void setService(IBinder service) {
93         synchronized (mLock) {
94             mDlcService = service;
95         }
96     }
97 
98     @GuardedBy("mLock")
bindLocked()99     private boolean bindLocked() {
100         if (mDlcService != null || mServiceConnection != null) {
101             return true;
102         }
103 
104         mServiceConnection = new DlcServiceConnection();
105 
106         final Intent service = new Intent().setComponent(mComponentName);
107         final boolean bound = mContext.bindService(service, mServiceConnection,
108                 Context.BIND_AUTO_CREATE);
109 
110         if (bound) {
111             LogUtil.i(TAG, "Binding " + mComponentName.flattenToShortString());
112         } else {
113             // As per bindService() documentation, we still need to call unbindService()
114             // if binding fails.
115             mContext.unbindService(mServiceConnection);
116             mServiceConnection = null;
117             LogUtil.e(TAG, "Binding " + mComponentName.flattenToShortString() + " failed.");
118         }
119 
120         return bound;
121     }
122 
123     @GuardedBy("mLock")
unbindLocked()124     private void unbindLocked() {
125         if (mServiceConnection == null) {
126             return;
127         }
128 
129         LogUtil.i(TAG, "Unbinding " + mComponentName.flattenToShortString());
130 
131         mContext.unbindService(mServiceConnection);
132 
133         mDlcService = null;
134         mServiceConnection = null;
135     }
136 
unbind()137     private void unbind() {
138         synchronized (mLock) {
139             unbindLocked();
140         }
141     }
142 
call(Callable<T> callable)143     protected <T> ListenableFuture<T> call(Callable<T> callable) {
144         return mListeningExecutorService.submit(() -> {
145             synchronized (mLock) {
146                 if (bindLocked()) {
147                     while (mDlcService == null) {
148                         try {
149                             mLock.wait();
150                         } catch (InterruptedException e) {
151                             // Nothing to do.
152                         }
153                     }
154                     return callable.call();
155                 }
156             }
157             throw new Exception("Failed to call remote DLC API");
158         });
159     }
160 
161     void tearDown() {
162         unbind();
163         mContext = null;
164     }
165 }
166