1 /* 2 * Copyright (C) 2018 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.android.launcher3.util; 17 18 import static android.util.Base64.NO_PADDING; 19 import static android.util.Base64.NO_WRAP; 20 21 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 22 23 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_KEY; 24 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL; 25 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG; 26 27 import static org.junit.Assert.assertTrue; 28 29 import android.app.Instrumentation; 30 import android.app.blob.BlobHandle; 31 import android.app.blob.BlobStoreManager; 32 import android.content.Context; 33 import android.content.pm.LauncherApps; 34 import android.content.res.Resources; 35 import android.graphics.Point; 36 import android.os.AsyncTask; 37 import android.os.Handler; 38 import android.os.Looper; 39 import android.os.ParcelFileDescriptor.AutoCloseOutputStream; 40 import android.os.Process; 41 import android.os.UserHandle; 42 import android.provider.Settings; 43 import android.system.OsConstants; 44 import android.util.Base64; 45 import android.util.Log; 46 47 import androidx.test.uiautomator.UiDevice; 48 49 import com.android.launcher3.tapl.LauncherInstrumentation; 50 import com.android.launcher3.tapl.Workspace; 51 52 import org.junit.Assert; 53 54 import java.io.FileOutputStream; 55 import java.io.IOException; 56 import java.io.InputStream; 57 import java.io.OutputStream; 58 import java.security.MessageDigest; 59 import java.util.concurrent.Callable; 60 import java.util.concurrent.CountDownLatch; 61 import java.util.concurrent.ExecutorService; 62 import java.util.concurrent.FutureTask; 63 import java.util.concurrent.TimeUnit; 64 import java.util.concurrent.TimeoutException; 65 66 public class TestUtil { 67 private static final String TAG = "TestUtil"; 68 69 public static final String DUMMY_PACKAGE = "com.example.android.aardwolf"; 70 public static final String DUMMY_CLASS_NAME = "com.example.android.aardwolf.Activity1"; 71 public static final long DEFAULT_UI_TIMEOUT = 10000; 72 installDummyApp()73 public static void installDummyApp() throws IOException { 74 final int defaultUserId = getMainUserId(); 75 installDummyAppForUser(defaultUserId); 76 } 77 installDummyAppForUser(int userId)78 public static void installDummyAppForUser(int userId) throws IOException { 79 Instrumentation instrumentation = getInstrumentation(); 80 // Copy apk from resources to a local file and install from there. 81 final Resources resources = instrumentation.getContext().getResources(); 82 final InputStream in = resources.openRawResource( 83 resources.getIdentifier("aardwolf_dummy_app", 84 "raw", instrumentation.getContext().getPackageName())); 85 final String apkFilename = instrumentation.getTargetContext() 86 .getFilesDir().getPath() + "/dummy_app.apk"; 87 88 try (PackageInstallCheck pic = new PackageInstallCheck()) { 89 final FileOutputStream out = new FileOutputStream(apkFilename); 90 byte[] buff = new byte[1024]; 91 int read; 92 93 while ((read = in.read(buff)) > 0) { 94 out.write(buff, 0, read); 95 } 96 in.close(); 97 out.close(); 98 99 final String result = UiDevice.getInstance(instrumentation) 100 .executeShellCommand(String.format("pm install -i %s --user ", 101 instrumentation.getContext().getPackageName()) 102 + userId + " " + apkFilename); 103 Assert.assertTrue( 104 "Failed to install wellbeing test apk; make sure the device is rooted", 105 "Success".equals(result.replaceAll("\\s+", ""))); 106 pic.mAddWait.await(); 107 } catch (InterruptedException e) { 108 throw new IOException(e); 109 } 110 } 111 112 /** 113 * Returns the main user ID. NOTE: For headless system it is NOT 0. Returns 0 by default, if 114 * there is no main user. 115 * 116 * @return a main user ID 117 */ getMainUserId()118 public static int getMainUserId() throws IOException { 119 Instrumentation instrumentation = getInstrumentation(); 120 final String result = UiDevice.getInstance(instrumentation) 121 .executeShellCommand("cmd user get-main-user"); 122 try { 123 return Integer.parseInt(result.trim()); 124 } catch (NumberFormatException e) { 125 return 0; 126 } 127 } 128 129 /** 130 * @return Grid coordinates from the center and corners of the Workspace. Those are not pixels. 131 * See {@link Workspace#getIconGridDimensions()} 132 */ getCornersAndCenterPositions(LauncherInstrumentation launcher)133 public static Point[] getCornersAndCenterPositions(LauncherInstrumentation launcher) { 134 final Point dimensions = launcher.getWorkspace().getIconGridDimensions(); 135 return new Point[]{ 136 new Point(0, 1), 137 new Point(0, dimensions.y - 2), 138 new Point(dimensions.x - 1, 1), 139 new Point(dimensions.x - 1, dimensions.y - 2), 140 new Point(dimensions.x / 2, dimensions.y / 2) 141 }; 142 } 143 uninstallDummyApp()144 public static void uninstallDummyApp() throws IOException { 145 UiDevice.getInstance(getInstrumentation()).executeShellCommand( 146 "pm uninstall " + DUMMY_PACKAGE); 147 } 148 149 /** 150 * Sets the default layout for Launcher and returns an object which can be used to clear 151 * the data 152 */ setLauncherDefaultLayout( Context context, LauncherLayoutBuilder layoutBuilder)153 public static AutoCloseable setLauncherDefaultLayout( 154 Context context, LauncherLayoutBuilder layoutBuilder) throws Exception { 155 byte[] data = layoutBuilder.build().getBytes(); 156 byte[] digest = MessageDigest.getInstance("SHA-256").digest(data); 157 158 BlobHandle handle = BlobHandle.createWithSha256( 159 digest, LAYOUT_DIGEST_LABEL, 0, LAYOUT_DIGEST_TAG); 160 BlobStoreManager blobManager = context.getSystemService(BlobStoreManager.class); 161 final long sessionId = blobManager.createSession(handle); 162 CountDownLatch wait = new CountDownLatch(1); 163 try (BlobStoreManager.Session session = blobManager.openSession(sessionId)) { 164 try (OutputStream out = new AutoCloseOutputStream(session.openWrite(0, -1))) { 165 out.write(data); 166 } 167 session.allowPublicAccess(); 168 session.commit(AsyncTask.THREAD_POOL_EXECUTOR, i -> wait.countDown()); 169 } 170 171 String key = Base64.encodeToString(digest, NO_WRAP | NO_PADDING); 172 Settings.Secure.putString(context.getContentResolver(), LAYOUT_DIGEST_KEY, key); 173 wait.await(); 174 return () -> 175 Settings.Secure.putString(context.getContentResolver(), LAYOUT_DIGEST_KEY, null); 176 } 177 178 /** 179 * Utility method to run a task synchronously which converts any exceptions to RuntimeException 180 */ runOnExecutorSync(ExecutorService executor, UncheckedRunnable task)181 public static void runOnExecutorSync(ExecutorService executor, UncheckedRunnable task) { 182 try { 183 executor.submit(() -> { 184 try { 185 task.run(); 186 } catch (Exception e) { 187 throw new RuntimeException(e); 188 } 189 }).get(); 190 } catch (Exception e) { 191 throw new RuntimeException(e); 192 } 193 } 194 195 /** 196 * Runs the callback on the UI thread and returns the result. 197 */ getOnUiThread(final Callable<T> callback)198 public static <T> T getOnUiThread(final Callable<T> callback) { 199 try { 200 FutureTask<T> task = new FutureTask<>(callback); 201 if (Looper.myLooper() == Looper.getMainLooper()) { 202 task.run(); 203 } else { 204 new Handler(Looper.getMainLooper()).post(task); 205 } 206 return task.get(DEFAULT_UI_TIMEOUT, TimeUnit.MILLISECONDS); 207 } catch (TimeoutException e) { 208 Log.e(TAG, "Timeout in getOnUiThread, sending SIGABRT", e); 209 Process.sendSignal(Process.myPid(), OsConstants.SIGABRT); 210 throw new RuntimeException(e); 211 } catch (Throwable e) { 212 throw new RuntimeException(e); 213 } 214 } 215 216 // Please don't add negative test cases for methods that fail only after a long wait. expectFail(String message, Runnable action)217 public static void expectFail(String message, Runnable action) { 218 boolean failed = false; 219 try { 220 action.run(); 221 } catch (AssertionError e) { 222 failed = true; 223 } 224 assertTrue(message, failed); 225 } 226 227 /** Interface to indicate a runnable which can throw any exception. */ 228 public interface UncheckedRunnable { 229 /** Method to run the task */ run()230 void run() throws Exception; 231 } 232 233 private static class PackageInstallCheck extends LauncherApps.Callback 234 implements AutoCloseable { 235 236 final CountDownLatch mAddWait = new CountDownLatch(1); 237 final LauncherApps mLauncherApps; 238 PackageInstallCheck()239 PackageInstallCheck() { 240 mLauncherApps = getInstrumentation().getTargetContext() 241 .getSystemService(LauncherApps.class); 242 mLauncherApps.registerCallback(this, new Handler(Looper.getMainLooper())); 243 } 244 verifyPackage(String packageName)245 private void verifyPackage(String packageName) { 246 if (DUMMY_PACKAGE.equals(packageName)) { 247 mAddWait.countDown(); 248 } 249 } 250 251 @Override onPackageAdded(String packageName, UserHandle user)252 public void onPackageAdded(String packageName, UserHandle user) { 253 verifyPackage(packageName); 254 } 255 256 @Override onPackageChanged(String packageName, UserHandle user)257 public void onPackageChanged(String packageName, UserHandle user) { 258 verifyPackage(packageName); 259 } 260 261 @Override onPackageRemoved(String packageName, UserHandle user)262 public void onPackageRemoved(String packageName, UserHandle user) { } 263 264 @Override onPackagesAvailable(String[] packageNames, UserHandle user, boolean replacing)265 public void onPackagesAvailable(String[] packageNames, UserHandle user, boolean replacing) { 266 for (String packageName : packageNames) { 267 verifyPackage(packageName); 268 } 269 } 270 271 @Override onPackagesUnavailable(String[] packageNames, UserHandle user, boolean replacing)272 public void onPackagesUnavailable(String[] packageNames, UserHandle user, 273 boolean replacing) { } 274 275 @Override close()276 public void close() { 277 mLauncherApps.unregisterCallback(this); 278 } 279 } 280 } 281