1 /*
2  * Copyright (C) 2022 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 android.app.sdksandbox.hosttestutils;
18 
19 import static org.junit.Assert.assertTrue;
20 
21 import com.android.tradefed.log.LogUtil;
22 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
23 import com.android.tradefed.util.RunUtil;
24 
25 import java.util.Arrays;
26 
27 public class AdoptableStorageUtils {
28 
29     private final BaseHostJUnit4Test mTest;
30 
31     private String mDiskId;
32 
AdoptableStorageUtils(BaseHostJUnit4Test test)33     public AdoptableStorageUtils(BaseHostJUnit4Test test) {
34         mTest = test;
35     }
36 
isAdoptableStorageSupported()37     public boolean isAdoptableStorageSupported() throws Exception {
38         boolean hasFeature =
39                 mTest.getDevice().hasFeature("feature:android.software.adoptable_storage");
40         boolean hasFstab =
41                 Boolean.parseBoolean(
42                         mTest.getDevice().executeShellCommand("sm has-adoptable").trim());
43         return hasFeature || hasFstab;
44     }
45 
46     // Creates a new volume in adoptable storage and returns its uuid
createNewVolume()47     public String createNewVolume() throws Exception {
48         mDiskId = getAdoptionDisk();
49         assertEmpty(mTest.getDevice().executeShellCommand("sm partition " + mDiskId + " private"));
50         final LocalVolumeInfo vol = getAdoptionVolume();
51         return vol.uuid;
52     }
53 
54     /**
55      * Enable a virtual disk to give us the best shot at being able to pass various tests. This
56      * helps verify devices that may not currently have an SD card inserted.
57      */
enableVirtualDisk()58     public void enableVirtualDisk() throws Exception {
59         if (isAdoptableStorageSupported()) {
60             mTest.getDevice().executeShellCommand("sm set-virtual-disk true");
61 
62             // Ensure virtual disk is mounted.
63             int attempt = 0;
64             boolean hasVirtualDisk = false;
65             String result = "";
66             while (!hasVirtualDisk && attempt++ < 50) {
67                 RunUtil.getDefault().sleep(1000);
68                 result = mTest.getDevice().executeShellCommand("sm list-disks adoptable").trim();
69                 hasVirtualDisk = result.startsWith("disk:");
70             }
71             assertTrue("Virtual disk is not ready: " + result, hasVirtualDisk);
72 
73             waitForVolumeReadyNoCheckingOrEjecting();
74         }
75     }
76 
77     // Destroy the volume created before
cleanUpVolume()78     public void cleanUpVolume() throws Exception {
79         mTest.getDevice().executeShellCommand("sm partition " + mDiskId + " public");
80         mTest.getDevice().executeShellCommand("sm forget all");
81     }
82 
getAdoptionDisk()83     private String getAdoptionDisk() throws Exception {
84         // In the case where we run multiple test we cleanup the state of the device. This
85         // results in the execution of sm forget all which causes the MountService to "reset"
86         // all its knowledge about available drives. This can cause the adoptable drive to
87         // become temporarily unavailable.
88         int attempt = 0;
89         String disks = mTest.getDevice().executeShellCommand("sm list-disks adoptable");
90         while ((disks == null || disks.isEmpty()) && attempt++ < 15) {
91             Thread.sleep(1000);
92             disks = mTest.getDevice().executeShellCommand("sm list-disks adoptable");
93         }
94 
95         if (disks == null || disks.isEmpty()) {
96             throw new AssertionError(
97                     "Devices that claim to support adoptable storage must have "
98                             + "adoptable media inserted during CTS to verify correct behavior");
99         }
100         return disks.split("\n")[0].trim();
101     }
102 
assertEmpty(String str)103     private static void assertEmpty(String str) {
104         if (str != null && str.trim().length() > 0) {
105             throw new AssertionError("Expected empty string but found " + str);
106         }
107     }
108 
109     private static class LocalVolumeInfo {
110         public String volId;
111         public String state;
112         public String uuid;
113 
LocalVolumeInfo(String line)114         LocalVolumeInfo(String line) {
115             final String[] split = line.split(" ");
116             volId = split[0];
117             state = split[1];
118             uuid = split[2];
119         }
120     }
121 
getAdoptionVolume()122     private LocalVolumeInfo getAdoptionVolume() throws Exception {
123         String[] lines = null;
124         int attempt = 0;
125         int mounted_count = 0;
126         while (attempt++ < 15) {
127             lines = mTest.getDevice().executeShellCommand("sm list-volumes private").split("\n");
128             LogUtil.CLog.w("getAdoptionVolume(): " + Arrays.toString(lines));
129             for (String line : lines) {
130                 final LocalVolumeInfo info = new LocalVolumeInfo(line.trim());
131                 if (!"private".equals(info.volId)) {
132                     if ("mounted".equals(info.state)) {
133                         // make sure the storage is mounted and stable for a while
134                         mounted_count++;
135                         attempt--;
136                         if (mounted_count >= 3) {
137                             return waitForVolumeReady(info);
138                         }
139                     } else {
140                         mounted_count = 0;
141                     }
142                 }
143             }
144             Thread.sleep(1000);
145         }
146         throw new AssertionError("Expected private volume; found " + Arrays.toString(lines));
147     }
148 
waitForVolumeReady(LocalVolumeInfo vol)149     private LocalVolumeInfo waitForVolumeReady(LocalVolumeInfo vol) throws Exception {
150         int attempt = 0;
151         while (attempt++ < 15) {
152             if (mTest.getDevice()
153                     .executeShellCommand("dumpsys package volumes")
154                     .contains(vol.volId)) {
155                 return vol;
156             }
157             Thread.sleep(1000);
158         }
159         throw new AssertionError("Volume not ready " + vol.volId);
160     }
161 
162     /** Ensure no volume is in ejecting or checking state */
waitForVolumeReadyNoCheckingOrEjecting()163     private void waitForVolumeReadyNoCheckingOrEjecting() throws Exception {
164         int attempt = 0;
165         boolean noCheckingEjecting = false;
166         String result = "";
167         while (!noCheckingEjecting && attempt++ < 600) {
168             result = mTest.getDevice().executeShellCommand("sm list-volumes");
169             noCheckingEjecting = !result.contains("ejecting") && !result.contains("checking");
170             RunUtil.getDefault().sleep(100);
171         }
172         assertTrue("Volumes are not ready: " + result, noCheckingEjecting);
173     }
174 }
175