1 /*
2  * Copyright (C) 2017 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 package com.google.android.car.kitchensink.storagelifetime;
17 
18 import static android.system.OsConstants.O_APPEND;
19 import static android.system.OsConstants.O_RDWR;
20 
21 import android.annotation.Nullable;
22 import android.car.Car;
23 import android.car.storagemonitoring.CarStorageMonitoringManager;
24 import android.car.storagemonitoring.CarStorageMonitoringManager.IoStatsListener;
25 import android.car.storagemonitoring.IoStats;
26 import android.car.storagemonitoring.IoStatsEntry;
27 import android.os.Bundle;
28 import android.os.StatFs;
29 import android.system.ErrnoException;
30 import android.system.Os;
31 import android.util.Log;
32 import android.view.LayoutInflater;
33 import android.view.View;
34 import android.view.ViewGroup;
35 import android.widget.ArrayAdapter;
36 import android.widget.ListView;
37 import android.widget.TextView;
38 
39 import androidx.fragment.app.Fragment;
40 
41 import com.google.android.car.kitchensink.KitchenSinkActivity;
42 import com.google.android.car.kitchensink.R;
43 
44 import java.io.File;
45 import java.io.FileDescriptor;
46 import java.io.IOException;
47 import java.nio.ByteBuffer;
48 import java.nio.file.Files;
49 import java.nio.file.Path;
50 import java.nio.file.StandardOpenOption;
51 import java.security.NoSuchAlgorithmException;
52 import java.security.SecureRandom;
53 import java.util.List;
54 
55 public class StorageLifetimeFragment extends Fragment {
56     private static final String FILE_NAME = "storage.bin";
57     private static final String TAG = "CAR.STORAGELIFETIME.KS";
58 
59     private static final int KILOBYTE = 1024;
60     private static final int MEGABYTE = 1024 * 1024;
61 
62     private StatFs mStatFs;
63     private KitchenSinkActivity mActivity;
64     private TextView mStorageWearInfo;
65     private ListView mStorageChangesHistory;
66     private TextView mFreeSpaceInfo;
67     private TextView mIoActivity;
68     private CarStorageMonitoringManager mStorageManager;
69 
70     private final IoStatsListener mIoListener = new IoStatsListener() {
71         @Override
72         public void onSnapshot(IoStats snapshot) {
73             if (mIoActivity != null) {
74                 mIoActivity.setText("");
75                 snapshot.getStats().forEach(uidIoStats -> {
76                     final long bytesWrittenToStorage = uidIoStats.foreground.bytesWrittenToStorage +
77                             uidIoStats.background.bytesWrittenToStorage;
78                     final long fsyncCalls = uidIoStats.foreground.fsyncCalls +
79                             uidIoStats.background.fsyncCalls;
80                     if (bytesWrittenToStorage > 0 || fsyncCalls > 0) {
81                         mIoActivity.append(String.format(
82                             "uid = %d, runtime = %d, bytes writen to disk = %d, fsync calls = %d\n",
83                             uidIoStats.uid,
84                             uidIoStats.runtimeMillis,
85                             bytesWrittenToStorage,
86                             fsyncCalls));
87                     }
88                 });
89                 final List<IoStatsEntry> totals = mStorageManager.getAggregateIoStats();
90 
91                 final long totalBytesWrittenToStorage = totals.stream()
92                         .mapToLong(stats -> stats.foreground.bytesWrittenToStorage +
93                                 stats.background.bytesWrittenToStorage)
94                         .reduce(0L, (x,y)->x+y);
95                 final long totalFsyncCalls = totals.stream()
96                         .mapToLong(stats -> stats.foreground.fsyncCalls +
97                             stats.background.fsyncCalls)
98                         .reduce(0L, (x,y)->x+y);
99 
100                 mIoActivity.append(String.format(
101                         "total bytes written to disk = %d, total fsync calls = %d",
102                         totalBytesWrittenToStorage,
103                         totalFsyncCalls));
104             }
105         }
106     };
107 
108     // TODO(egranata): put this somewhere more useful than KitchenSink
preEolToString(int preEol)109     private static String preEolToString(int preEol) {
110         switch (preEol) {
111             case 1: return "normal";
112             case 2: return "warning";
113             case 3: return "urgent";
114             default:
115                 return "unknown";
116         }
117     }
118 
getFilePath()119     private Path getFilePath() throws IOException {
120         Path filePath = new File(mActivity.getFilesDir(), FILE_NAME).toPath();
121         if (Files.notExists(filePath)) {
122             Files.createFile(filePath);
123         }
124         return filePath;
125     }
126 
writeBytesToFile(int size)127     private void writeBytesToFile(int size) {
128         try {
129             final Path filePath = getFilePath();
130             byte[] data = new byte[size];
131             SecureRandom.getInstanceStrong().nextBytes(data);
132             Files.write(filePath,
133                 data,
134                 StandardOpenOption.APPEND);
135         } catch (NoSuchAlgorithmException | IOException e) {
136             Log.w(TAG, "could not append data", e);
137         }
138     }
139 
fsyncFile()140     private void fsyncFile() {
141         try {
142             final Path filePath = getFilePath();
143             FileDescriptor fd = Os.open(filePath.toString(), O_APPEND | O_RDWR, 0);
144             if (!fd.valid()) {
145                 Log.w(TAG, "file descriptor is invalid");
146                 return;
147             }
148             // fill byteBuffer with arbitrary data in order to make an fsync() meaningful
149             ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[] {101, 110, 114, 105, 99, 111});
150             Os.write(fd, byteBuffer);
151             Os.fsync(fd);
152             Os.close(fd);
153         } catch (ErrnoException | IOException e) {
154             Log.w(TAG, "could not fsync data", e);
155         }
156     }
157 
158     @Nullable
159     @Override
onCreateView( LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)160     public View onCreateView(
161             LayoutInflater inflater,
162             @Nullable ViewGroup container,
163             @Nullable Bundle savedInstanceState) {
164         View view = inflater.inflate(R.layout.storagewear, container, false);
165         mActivity = (KitchenSinkActivity) getHost();
166         mStorageWearInfo = view.findViewById(R.id.storage_wear_info);
167         mStorageChangesHistory = view.findViewById(R.id.storage_events_list);
168         mFreeSpaceInfo = view.findViewById(R.id.free_disk_space);
169         mIoActivity = view.findViewById(R.id.last_io_snapshot);
170 
171         view.findViewById(R.id.write_one_kilobyte).setOnClickListener(
172             v -> writeBytesToFile(KILOBYTE));
173 
174         view.findViewById(R.id.write_one_megabyte).setOnClickListener(
175             v -> writeBytesToFile(MEGABYTE));
176 
177         view.findViewById(R.id.perform_fsync).setOnClickListener(
178             v -> fsyncFile());
179 
180         return view;
181     }
182 
reloadInfo()183     private void reloadInfo() {
184         mStatFs = new StatFs(mActivity.getFilesDir().getAbsolutePath());
185 
186         mStorageManager =
187             (CarStorageMonitoringManager) mActivity.getCar().getCarManager(
188                     Car.STORAGE_MONITORING_SERVICE);
189 
190         mStorageWearInfo.setText("Wear estimate: " + mStorageManager.getWearEstimate()
191                 + "\nPre EOL indicator: "
192                 + preEolToString(mStorageManager.getPreEolIndicatorStatus()));
193 
194         mStorageChangesHistory.setAdapter(new ArrayAdapter(mActivity,
195                 R.layout.wear_estimate_change_textview,
196                 mStorageManager.getWearEstimateHistory().toArray()));
197 
198         mFreeSpaceInfo.setText("Available blocks: " + mStatFs.getAvailableBlocksLong()
199                 + "\nBlock size: " + mStatFs.getBlockSizeLong() + " bytes"
200                 + "\nfor a total free space of: "
201                 + (mStatFs.getBlockSizeLong() * mStatFs.getAvailableBlocksLong() / MEGABYTE)
202                 + "MB");
203     }
204 
registerListener()205     private void registerListener() {
206         mStorageManager.registerListener(mIoListener);
207     }
208 
unregisterListener()209     private void unregisterListener() {
210         mStorageManager.unregisterListener(mIoListener);
211     }
212 
213     @Override
onResume()214     public void onResume() {
215         super.onResume();
216         if (!mActivity.getCar().isFeatureEnabled(Car.STORAGE_MONITORING_SERVICE)) {
217             Log.w(TAG, "STORAGE_MONITORING_SERVICE not supported");
218             return;
219         }
220         reloadInfo();
221         registerListener();
222     }
223 
224     @Override
onPause()225     public void onPause() {
226         if (mActivity.getCar().isFeatureEnabled(Car.STORAGE_MONITORING_SERVICE)) {
227             unregisterListener();
228         }
229         super.onPause();
230     }
231 }
232