1 /*
2  * Copyright (C) 2014 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.server.net;
18 
19 import android.os.Handler;
20 import android.os.HandlerThread;
21 import android.text.TextUtils;
22 import android.util.Log;
23 
24 import com.android.internal.annotations.VisibleForTesting;
25 
26 import java.io.BufferedOutputStream;
27 import java.io.DataOutputStream;
28 import java.io.FileOutputStream;
29 import java.io.IOException;
30 
31 /**
32  * This class provides APIs to do a delayed data write to a given {@link OutputStream}.
33  */
34 public class DelayedDiskWrite {
35     private static final String TAG = "DelayedDiskWrite";
36 
37     private final Dependencies mDeps;
38 
39     private HandlerThread mDiskWriteHandlerThread;
40     private Handler mDiskWriteHandler;
41     /* Tracks multiple writes on the same thread */
42     private int mWriteSequence = 0;
43 
DelayedDiskWrite()44     public DelayedDiskWrite() {
45         this(new Dependencies());
46     }
47 
48     @VisibleForTesting
DelayedDiskWrite(Dependencies deps)49     DelayedDiskWrite(Dependencies deps) {
50         mDeps = deps;
51     }
52 
53     /**
54      * Dependencies class of DelayedDiskWrite, used for injection in tests.
55      */
56     @VisibleForTesting
57     static class Dependencies {
58         /**
59          * Create a HandlerThread to use in DelayedDiskWrite.
60          */
makeHandlerThread()61         public HandlerThread makeHandlerThread() {
62             return new HandlerThread("DelayedDiskWriteThread");
63         }
64 
65         /**
66          * Quit the HandlerThread looper.
67          */
quitHandlerThread(HandlerThread handlerThread)68         public void quitHandlerThread(HandlerThread handlerThread) {
69             handlerThread.getLooper().quit();
70         }
71     }
72 
73     /**
74      * Used to do a delayed data write to a given {@link OutputStream}.
75      */
76     public interface Writer {
77         /**
78          * write data to a given {@link OutputStream}.
79          */
onWriteCalled(DataOutputStream out)80         void onWriteCalled(DataOutputStream out) throws IOException;
81     }
82 
83     /**
84      * Do a delayed data write to a given output stream opened from filePath.
85      */
write(final String filePath, final Writer w)86     public void write(final String filePath, final Writer w) {
87         write(filePath, w, true);
88     }
89 
90     /**
91      * Do a delayed data write to a given output stream opened from filePath.
92      */
write(final String filePath, final Writer w, final boolean open)93     public void write(final String filePath, final Writer w, final boolean open) {
94         if (TextUtils.isEmpty(filePath)) {
95             throw new IllegalArgumentException("empty file path");
96         }
97 
98         /* Do a delayed write to disk on a separate handler thread */
99         synchronized (this) {
100             if (++mWriteSequence == 1) {
101                 mDiskWriteHandlerThread = mDeps.makeHandlerThread();
102                 mDiskWriteHandlerThread.start();
103                 mDiskWriteHandler = new Handler(mDiskWriteHandlerThread.getLooper());
104             }
105         }
106 
107         mDiskWriteHandler.post(new Runnable() {
108             @Override
109             public void run() {
110                 doWrite(filePath, w, open);
111             }
112         });
113     }
114 
doWrite(String filePath, Writer w, boolean open)115     private void doWrite(String filePath, Writer w, boolean open) {
116         DataOutputStream out = null;
117         try {
118             if (open) {
119                 out = new DataOutputStream(new BufferedOutputStream(
120                         new FileOutputStream(filePath)));
121             }
122             w.onWriteCalled(out);
123         } catch (IOException e) {
124             loge("Error writing data file " + filePath);
125         } finally {
126             if (out != null) {
127                 try {
128                     out.close();
129                 } catch (Exception e) { }
130             }
131 
132             // Quit if no more writes sent
133             synchronized (this) {
134                 if (--mWriteSequence == 0) {
135                     mDeps.quitHandlerThread(mDiskWriteHandlerThread);
136                     mDiskWriteHandlerThread = null;
137                     mDiskWriteHandler = null;
138                 }
139             }
140         }
141     }
142 
loge(String s)143     private void loge(String s) {
144         Log.e(TAG, s);
145     }
146 }
147 
148