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 android.app.Instrumentation; 28 import android.app.blob.BlobHandle; 29 import android.app.blob.BlobStoreManager; 30 import android.content.Context; 31 import android.content.pm.LauncherApps; 32 import android.content.res.Resources; 33 import android.os.AsyncTask; 34 import android.os.Handler; 35 import android.os.Looper; 36 import android.os.ParcelFileDescriptor.AutoCloseOutputStream; 37 import android.os.Process; 38 import android.os.UserHandle; 39 import android.provider.Settings; 40 import android.system.OsConstants; 41 import android.util.Base64; 42 import android.util.Log; 43 44 import androidx.test.uiautomator.UiDevice; 45 46 import com.android.launcher3.config.FeatureFlags; 47 import com.android.launcher3.config.FeatureFlags.BooleanFlag; 48 import com.android.launcher3.config.FeatureFlags.IntFlag; 49 50 import org.junit.Assert; 51 52 import java.io.FileOutputStream; 53 import java.io.IOException; 54 import java.io.InputStream; 55 import java.io.OutputStream; 56 import java.security.MessageDigest; 57 import java.util.concurrent.Callable; 58 import java.util.concurrent.CountDownLatch; 59 import java.util.concurrent.ExecutorService; 60 import java.util.concurrent.FutureTask; 61 import java.util.concurrent.TimeUnit; 62 import java.util.concurrent.TimeoutException; 63 import java.util.function.Predicate; 64 import java.util.function.ToIntFunction; 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("pm install --user " + userId + " " + apkFilename); 101 Assert.assertTrue( 102 "Failed to install wellbeing test apk; make sure the device is rooted", 103 "Success".equals(result.replaceAll("\\s+", ""))); 104 pic.mAddWait.await(); 105 } catch (InterruptedException e) { 106 throw new IOException(e); 107 } 108 } 109 110 /** 111 * Returns the main user ID. NOTE: For headless system it is NOT 0. Returns 0 by default, if 112 * there is no main user. 113 * 114 * @return a main user ID 115 */ getMainUserId()116 public static int getMainUserId() throws IOException { 117 Instrumentation instrumentation = getInstrumentation(); 118 final String result = UiDevice.getInstance(instrumentation) 119 .executeShellCommand("cmd user get-main-user"); 120 try { 121 return Integer.parseInt(result.trim()); 122 } catch (NumberFormatException e) { 123 return 0; 124 } 125 } 126 127 /** 128 * Utility class to override a boolean flag during test. Note that the returned SafeCloseable 129 * must be closed to restore the original state 130 */ overrideFlag(BooleanFlag flag, boolean value)131 public static SafeCloseable overrideFlag(BooleanFlag flag, boolean value) { 132 Predicate<BooleanFlag> originalProxy = FeatureFlags.sBooleanReader; 133 Predicate<BooleanFlag> testProxy = f -> f == flag ? value : originalProxy.test(f); 134 FeatureFlags.sBooleanReader = testProxy; 135 return () -> { 136 if (FeatureFlags.sBooleanReader == testProxy) { 137 FeatureFlags.sBooleanReader = originalProxy; 138 } 139 }; 140 } 141 142 /** 143 * Utility class to override a int flag during test. Note that the returned SafeCloseable 144 * must be closed to restore the original state 145 */ 146 public static SafeCloseable overrideFlag(IntFlag flag, int value) { 147 ToIntFunction<IntFlag> originalProxy = FeatureFlags.sIntReader; 148 ToIntFunction<IntFlag> testProxy = f -> f == flag ? value : originalProxy.applyAsInt(f); 149 FeatureFlags.sIntReader = testProxy; 150 return () -> { 151 if (FeatureFlags.sIntReader == testProxy) { 152 FeatureFlags.sIntReader = originalProxy; 153 } 154 }; 155 } 156 157 public static void uninstallDummyApp() throws IOException { 158 UiDevice.getInstance(getInstrumentation()).executeShellCommand( 159 "pm uninstall " + DUMMY_PACKAGE); 160 } 161 162 /** 163 * Sets the default layout for Launcher and returns an object which can be used to clear 164 * the data 165 */ 166 public static AutoCloseable setLauncherDefaultLayout( 167 Context context, LauncherLayoutBuilder layoutBuilder) throws Exception { 168 byte[] data = layoutBuilder.build().getBytes(); 169 byte[] digest = MessageDigest.getInstance("SHA-256").digest(data); 170 171 BlobHandle handle = BlobHandle.createWithSha256( 172 digest, LAYOUT_DIGEST_LABEL, 0, LAYOUT_DIGEST_TAG); 173 BlobStoreManager blobManager = context.getSystemService(BlobStoreManager.class); 174 final long sessionId = blobManager.createSession(handle); 175 CountDownLatch wait = new CountDownLatch(1); 176 try (BlobStoreManager.Session session = blobManager.openSession(sessionId)) { 177 try (OutputStream out = new AutoCloseOutputStream(session.openWrite(0, -1))) { 178 out.write(data); 179 } 180 session.allowPublicAccess(); 181 session.commit(AsyncTask.THREAD_POOL_EXECUTOR, i -> wait.countDown()); 182 } 183 184 String key = Base64.encodeToString(digest, NO_WRAP | NO_PADDING); 185 Settings.Secure.putString(context.getContentResolver(), LAYOUT_DIGEST_KEY, key); 186 wait.await(); 187 return () -> 188 Settings.Secure.putString(context.getContentResolver(), LAYOUT_DIGEST_KEY, null); 189 } 190 191 /** 192 * Utility method to run a task synchronously which converts any exceptions to RuntimeException 193 */ 194 public static void runOnExecutorSync(ExecutorService executor, UncheckedRunnable task) { 195 try { 196 executor.submit(() -> { 197 try { 198 task.run(); 199 } catch (Exception e) { 200 throw new RuntimeException(e); 201 } 202 }).get(); 203 } catch (Exception e) { 204 throw new RuntimeException(e); 205 } 206 } 207 208 /** 209 * Runs the callback on the UI thread and returns the result. 210 */ 211 public static <T> T getOnUiThread(final Callable<T> callback) { 212 try { 213 FutureTask<T> task = new FutureTask<>(callback); 214 if (Looper.myLooper() == Looper.getMainLooper()) { 215 task.run(); 216 } else { 217 new Handler(Looper.getMainLooper()).post(task); 218 } 219 return task.get(DEFAULT_UI_TIMEOUT, TimeUnit.MILLISECONDS); 220 } catch (TimeoutException e) { 221 Log.e(TAG, "Timeout in getOnUiThread, sending SIGABRT", e); 222 Process.sendSignal(Process.myPid(), OsConstants.SIGABRT); 223 throw new RuntimeException(e); 224 } catch (Throwable e) { 225 throw new RuntimeException(e); 226 } 227 } 228 229 /** Interface to indicate a runnable which can throw any exception. */ 230 public interface UncheckedRunnable { 231 /** Method to run the task */ 232 void run() throws Exception; 233 } 234 235 private static class PackageInstallCheck extends LauncherApps.Callback 236 implements AutoCloseable { 237 238 final CountDownLatch mAddWait = new CountDownLatch(1); 239 final LauncherApps mLauncherApps; 240 241 PackageInstallCheck() { 242 mLauncherApps = getInstrumentation().getTargetContext() 243 .getSystemService(LauncherApps.class); 244 mLauncherApps.registerCallback(this, new Handler(Looper.getMainLooper())); 245 } 246 247 private void verifyPackage(String packageName) { 248 if (DUMMY_PACKAGE.equals(packageName)) { 249 mAddWait.countDown(); 250 } 251 } 252 253 @Override 254 public void onPackageAdded(String packageName, UserHandle user) { 255 verifyPackage(packageName); 256 } 257 258 @Override 259 public void onPackageChanged(String packageName, UserHandle user) { 260 verifyPackage(packageName); 261 } 262 263 @Override 264 public void onPackageRemoved(String packageName, UserHandle user) { } 265 266 @Override 267 public void onPackagesAvailable(String[] packageNames, UserHandle user, boolean replacing) { 268 for (String packageName : packageNames) { 269 verifyPackage(packageName); 270 } 271 } 272 273 @Override 274 public void onPackagesUnavailable(String[] packageNames, UserHandle user, 275 boolean replacing) { } 276 277 @Override 278 public void close() { 279 mLauncherApps.unregisterCallback(this); 280 } 281 } 282 } 283