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