1 /*
2  * Copyright (C) 2021 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.providers.media.tests.utils;
18 
19 import android.app.UiAutomation;
20 import android.os.Environment;
21 import android.system.ErrnoException;
22 import android.system.Os;
23 import android.util.Log;
24 
25 import androidx.test.InstrumentationRegistry;
26 
27 import com.google.common.io.ByteStreams;
28 
29 import java.io.File;
30 import java.io.FileInputStream;
31 import java.io.IOException;
32 import java.io.InterruptedIOException;
33 import java.util.concurrent.TimeUnit;
34 import java.util.concurrent.TimeoutException;
35 import java.util.function.Supplier;
36 
37 /**
38  * Helper methods for public volume setup.
39  */
40 public class PublicVolumeSetupHelper {
41     private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(2);
42     private static final long POLLING_SLEEP_MILLIS = 100;
43     private static final String TAG = "TestUtils";
44     private static boolean usingExistingPublicVolume = false;
45 
46     /**
47      * (Re-)partitions an already created pulic volume
48      */
partitionPublicVolume()49     public static void partitionPublicVolume() throws Exception {
50         pollForCondition(() -> partitionDisk(), "Timed out while waiting for"
51                 + " disk partitioning");
52         // Poll twice to avoid using previous mount status
53         pollForCondition(() -> isPublicVolumeMounted(), "Timed out while waiting for"
54                 + " the public volume to mount");
55     }
56 
57     /**
58      * Polls for external storage to be mounted.
59      */
pollForExternalStorageStateMounted()60     public static void pollForExternalStorageStateMounted() throws Exception {
61         pollForCondition(() -> isExternalStorageStateMounted(), "Timed out while"
62                 + " waiting for ExternalStorageState to be MEDIA_MOUNTED");
63     }
64 
65     /**
66      * Creates a new virtual public volume and returns the volume's name.
67      */
createNewPublicVolume()68     public static void createNewPublicVolume() throws Exception {
69         // Skip public volume setup if we can use already available public volume on the device.
70         if (getCurrentPublicVolumeString() != null && isPublicVolumeMounted()) {
71             usingExistingPublicVolume = true;
72             return;
73         }
74         executeShellCommand("sm set-force-adoptable on");
75         executeShellCommand("sm set-virtual-disk true");
76 
77         partitionPublicVolume();
78 
79         pollForExternalStorageStateMounted();
80     }
81 
isExternalStorageStateMounted()82     private static boolean isExternalStorageStateMounted() {
83         final File target = Environment.getExternalStorageDirectory();
84         try {
85             return (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState(target))
86                     && Os.statvfs(target.getAbsolutePath()).f_blocks > 0);
87         } catch (ErrnoException ignored) {
88         }
89         return false;
90     }
91 
isPublicVolumeMounted()92     private static boolean isPublicVolumeMounted() {
93         try {
94             final String publicVolume = executeShellCommand("sm list-volumes public").trim();
95             return publicVolume != null && publicVolume.contains("mounted");
96         } catch (Exception e) {
97             return false;
98         }
99     }
100 
partitionDisk()101     private static boolean partitionDisk() {
102         try {
103             final String listDisks = executeShellCommand("sm list-disks").trim();
104             if (listDisks.length() > 0) {
105                 executeShellCommand("sm partition " + listDisks + " public");
106                 return true;
107             }
108             return false;
109         } catch (Exception e) {
110             return false;
111         }
112     }
113 
114     /**
115      * Gets the name of the public volume string from list-volumes,
116      * waiting for a bit for it to be available.
117      */
getPublicVolumeString()118     private static String getPublicVolumeString() throws Exception {
119         final String[] volName = new String[1];
120         pollForCondition(() -> {
121             volName[0] = getCurrentPublicVolumeString();
122             return volName[0] != null;
123         }, "Timed out while waiting for public volume to be ready");
124 
125         return volName[0];
126     }
127 
128     /**
129      * @return the currently mounted public volume string, if any.
130      */
getCurrentPublicVolumeString()131     static String getCurrentPublicVolumeString() {
132         final String[] allPublicVolumeDetails;
133         try {
134             allPublicVolumeDetails = executeShellCommand("sm list-volumes public")
135                     .trim().split("\n");
136         } catch (Exception e) {
137             Log.e(TAG, "Failed to execute shell command", e);
138             return null;
139         }
140         for (String volDetails : allPublicVolumeDetails) {
141             if (volDetails.startsWith("public")) {
142                 final String[] publicVolumeDetails = volDetails.trim().split(" ");
143                 String res = publicVolumeDetails[0];
144                 if ("null".equals(res)) {
145                     continue;
146                 }
147                 return res;
148             }
149         }
150         return null;
151     }
152 
mountPublicVolume()153     public static void mountPublicVolume() throws Exception {
154         executeShellCommand("sm mount " + getPublicVolumeString());
155     }
156 
unmountPublicVolume()157     public static void unmountPublicVolume() throws Exception {
158         executeShellCommand("sm unmount " + getPublicVolumeString());
159     }
160 
deletePublicVolumes()161     public static void deletePublicVolumes() throws Exception {
162         if (!usingExistingPublicVolume) {
163             executeShellCommand("sm set-virtual-disk false");
164             // Wait for the public volume to disappear.
165             for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) {
166                 if (!isPublicVolumeMounted()) {
167                     return;
168                 }
169                 Thread.sleep(POLLING_SLEEP_MILLIS);
170             }
171         }
172     }
173 
174     /**
175      * Executes a shell command.
176      */
executeShellCommand(String pattern, Object...args)177     public static String executeShellCommand(String pattern, Object...args) throws IOException {
178         String command = String.format(pattern, args);
179         int attempt = 0;
180         while (attempt++ < 5) {
181             try {
182                 return executeShellCommandInternal(command);
183             } catch (InterruptedIOException e) {
184                 // Hmm, we had trouble executing the shell command; the best we
185                 // can do is try again a few more times
186                 Log.v(TAG, "Trouble executing " + command + "; trying again", e);
187             }
188         }
189         throw new IOException("Failed to execute " + command);
190     }
191 
executeShellCommandInternal(String cmd)192     private static String executeShellCommandInternal(String cmd) throws IOException {
193         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
194         try (FileInputStream output = new FileInputStream(
195                 uiAutomation.executeShellCommand(cmd).getFileDescriptor())) {
196             return new String(ByteStreams.toByteArray(output));
197         }
198     }
199 
pollForCondition(Supplier<Boolean> condition, String errorMessage)200     public static void pollForCondition(Supplier<Boolean> condition, String errorMessage)
201             throws Exception {
202         for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) {
203             if (condition.get()) {
204                 return;
205             }
206             Thread.sleep(POLLING_SLEEP_MILLIS);
207         }
208         throw new TimeoutException(errorMessage);
209     }
210 }
211