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