1 /*
2  * Copyright 2024 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 androidx.pdf.viewer.loader;
18 
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.ServiceConnection;
23 import android.net.Uri;
24 import android.os.IBinder;
25 
26 import androidx.annotation.RestrictTo;
27 import androidx.pdf.models.PdfDocumentRemote;
28 import androidx.pdf.service.PdfDocumentService;
29 import androidx.pdf.util.Preconditions;
30 
31 import org.jspecify.annotations.NonNull;
32 
33 import java.util.concurrent.locks.Condition;
34 import java.util.concurrent.locks.Lock;
35 import java.util.concurrent.locks.ReentrantLock;
36 
37 /**
38  * Handles the connection to the Pdf service:
39  * <ul>
40  * <li>Handles binding and the lifecycle of the connection,
41  * <li>Manages the {@link PdfDocumentRemote} stub object.
42  * </ul>
43  */
44 @RestrictTo(RestrictTo.Scope.LIBRARY)
45 public class PdfConnection implements ServiceConnection {
46     private static final String TAG = PdfConnection.class.getSimpleName();
47 
48     private static final int MAX_CONNECT_RETRIES = 3;
49 
50     private final Context mContext;
51     private final Lock mLock = new ReentrantLock();
52     private final Condition mIsBound = mLock.newCondition();
53     private PdfDocumentRemote mPdfRemote;
54     private int mNumCrashes = 0;
55     private boolean mHasSuccessfullyConnectedEver = false;
56     private boolean mIsLoaded = false;
57 
58     private String mCurrentTask = null;
59     private boolean mConnected = false;
60 
61     private Runnable mOnConnect;
62     private Runnable mOnConnectFailure;
63 
PdfConnection(Context ctx)64     PdfConnection(Context ctx) {
65         this.mContext = ctx;
66     }
67 
68     /** Sets a {@link Runnable} to be run as soon as the service is (re-)connected. */
setOnConnectInitializer(@onNull Runnable onConnect)69     public void setOnConnectInitializer(@NonNull Runnable onConnect) {
70         this.mOnConnect = onConnect;
71     }
72 
73     /** Sets a {@link Runnable} to be run if the service never successfully connects. */
setConnectionFailureHandler(@onNull Runnable onConnectFailure)74     public void setConnectionFailureHandler(@NonNull Runnable onConnectFailure) {
75         this.mOnConnectFailure = onConnectFailure;
76     }
77 
78     /** Checks if Connection to PdfDocumentService is established */
isConnected()79     public boolean isConnected() {
80         return mConnected;
81     }
82 
83     /**
84      * Returns a {@link PdfDocumentRemote} if the service is bound. It could be still initializing
85      * (see {@link #setDocumentLoaded}).
86      */
getPdfDocument(@onNull String forTask)87     public @NonNull PdfDocumentRemote getPdfDocument(@NonNull String forTask) {
88         Preconditions.checkState(mCurrentTask == null, "already locked: " + mCurrentTask);
89         mCurrentTask = forTask;
90         return mPdfRemote;
91     }
92 
93     /**
94      * Releases the Pdf Remote.
95      */
releasePdfDocument()96     public void releasePdfDocument() {
97         mCurrentTask = null;
98     }
99 
100     /** Returns whether {@link #getPdfDocument} is ready to accept tasks. */
isLoaded()101     protected boolean isLoaded() {
102         return mPdfRemote != null && mIsLoaded;
103     }
104 
105     /**
106      * This records that the document is loaded:
107      * <ul>
108      * <li>it is now ready to process tasks (until a disconnection notice happens),
109      * <li>since we were able to load this document once, we should be able to load it again if
110      * there is a problem and not run in a perpetual crash-restart loop.
111      * </ul>
112      */
setDocumentLoaded()113     public void setDocumentLoaded() {
114         if (mPdfRemote != null) {
115             mHasSuccessfullyConnectedEver = true;
116             mIsLoaded = true;
117         }
118     }
119 
120     @Override
onServiceConnected(ComponentName name, IBinder service)121     public void onServiceConnected(ComponentName name, IBinder service) {
122         mConnected = true;
123         mIsLoaded = false;
124         mLock.lock();
125         try {
126             mPdfRemote = PdfDocumentRemote.Stub.asInterface(service);
127             mIsBound.signal();
128         } finally {
129             mLock.unlock();
130         }
131         if (mOnConnect != null) {
132             mOnConnect.run();
133         }
134     }
135 
136     @Override
onServiceDisconnected(ComponentName name)137     public void onServiceDisconnected(ComponentName name) {
138         mIsLoaded = false;
139         if (mCurrentTask != null) {
140             // A task was in progress, we want to report the crash and restart the service.
141             mNumCrashes++;
142             TaskDenyList.maybeDenyListTask(mCurrentTask);
143 
144             // We have never connected to this document, and we have crashed repeatedly.
145             if (!mHasSuccessfullyConnectedEver && mNumCrashes >= MAX_CONNECT_RETRIES) {
146                 disconnect();
147                 if (mOnConnectFailure != null) {
148                     mOnConnectFailure.run();
149                 }
150             }
151         } else {
152             // No task was in progress, probably just system cleaning up idle resources.
153             disconnect();
154         }
155 
156         // if disconnect() was not called, the system will try to restart the service, and when
157         // it does,
158         // onServiceConnected will be called again.
159         mLock.lock();
160         try {
161             mPdfRemote = null;
162         } finally {
163             mLock.unlock();
164         }
165     }
166 
connect(Uri uri)167     void connect(Uri uri) {
168         if (mConnected) {
169             return;
170         }
171         Intent intent = new Intent(mContext, PdfDocumentService.class);
172         // Data is only required here to make sure we start a new service per document.
173         intent.setData(uri);
174         mContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
175     }
176 
disconnect()177     void disconnect() {
178         mLock.lock();
179         try {
180             if (mConnected) {
181                 mContext.unbindService(this);
182                 mConnected = false;
183             }
184         } finally {
185             mLock.unlock();
186         }
187     }
188 }
189