DexFile in 2.0 versions of Android Studio and Gradle - android

I am using the following code to instantiate all the classes included in a certain package.
DexFile df = new DexFile(getPackageCodePath());
for (Enumeration<String> iter = df.entries(); iter.hasMoreElements(); ) {
String className = iter.nextElement();
if (className.contains(packageName) && !className.contains("$")) {
myClasses.add(Class.forName(className).newInstance());
}
}
Unfortunately it is not working properly anymore. Since Android Studio 2 and Gradle 2.0.0, the DexFile entries no longer include all the classes within the app but only the classes belonging to the com.android.tools package.
Is this a known issue?

Looks like this issue is related to the new InstantRun feature in the Android Plugin for Gradle 2.0.0.
getPackageCodePath() gets a String pointing towards the base.apk file in the Android file system. If we unzip that apk we can find one or several .dex files inside its root folder. The entries obtained from the method df.entries() iterates over the .dex files found in that root folder in order to obtain all of its compiled classes.
However, if we are using the new Android Plugin for Gradle, we will only find the .dex related to the android runtime and instant run (packages com.tools.android.fd.runtime, com.tools.android.fd.common and com.tools.android.tools.ir.api). Every other class will be compiled in several .dex files, zipped into a file called instant-run.zip and placed into the root folder of the apk.
That's why the code posted in the question is not able to list all the classes within the app. Still, this will only affect Debug builds since the Release ones don't feature InstantRun.

To access all DexFiles you can do this
internal fun getDexFiles(context: Context): List<DexFile> {
// Here we do some reflection to access the dex files from the class loader. These implementation details vary by platform version,
// so we have to be a little careful, but not a huge deal since this is just for testing. It should work on 21+.
// The source for reference is at:
// https://android.googlesource.com/platform/libcore/+/oreo-release/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
val classLoader = context.classLoader as BaseDexClassLoader
val pathListField = field("dalvik.system.BaseDexClassLoader", "pathList")
val pathList = pathListField.get(classLoader) // Type is DexPathList
val dexElementsField = field("dalvik.system.DexPathList", "dexElements")
#Suppress("UNCHECKED_CAST")
val dexElements = dexElementsField.get(pathList) as Array<Any> // Type is Array<DexPathList.Element>
val dexFileField = field("dalvik.system.DexPathList\$Element", "dexFile")
return dexElements.map {
dexFileField.get(it) as DexFile
}
}
private fun field(className: String, fieldName: String): Field {
val clazz = Class.forName(className)
val field = clazz.getDeclaredField(fieldName)
field.isAccessible = true
return field
}

for get all dex files of an app use below method.
public static ArrayList<DexFile> getMultiDex()
{
BaseDexClassLoader dexLoader = (BaseDexClassLoader) getClassLoader();
Field f = getField("pathList", getClassByAddressName("dalvik.system.BaseDexClassLoader"));
Object pathList = getObjectFromField(f, dexLoader);
Field f2 = getField("dexElements", getClassByAddressName("dalvik.system.DexPathList"));
Object[] list = getObjectFromField(f2, pathList);
Field f3 = getField("dexFile", getClassByAddressName("dalvik.system.DexPathList$Element"));
ArrayList<DexFile> res = new ArrayList<>();
for(int i = 0; i < list.length; i++)
{
DexFile d = getObjectFromField(f3, list[i]);
res.add(d);
}
return res;
}
//------------ other methods
public static ClassLoader getClassLoader()
{
return Thread.currentThread().getContextClassLoader();
}
public static Class<?> getClassByAddressName(String classAddressName)
{
Class mClass = null;
try
{
mClass = Class.forName(classAddressName);
} catch(Exception e)
{
}
return mClass;
}
public static <T extends Object> T getObjectFromField(Field field, Object arg)
{
try
{
field.setAccessible(true);
return (T) field.get(arg);
} catch(Exception e)
{
e.printStackTrace();
return null;
}
}

Related

kotlin dotenv doesn't take directory configuration

Trying to use kotlin-dotnet, but this is not working, using kotlin object class, to manage singleton
Got error Could not find /asset/env on the classpath whereas the env file is in /assets folder as mentionned in the doc
object EnvVariables {
val envVar: Dotenv =
dotenv {
directory = "/assets"
filename = "env"
}
}
object RetrofitInstance {
val api: TodoService by lazy {
Retrofit.Builder()
.baseUrl(EnvVariables.envVar["BASE_URL"])
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(TodoService::class.java)
}
}
Most likely the doc is not accurate
In file DotEnvReader.java
return ClasspathHelper
.loadFileFromClasspath(location.replaceFirst("./", "/"))
.collect(Collectors.toList());
So, you can fix it by adding "."
var dotenv = dotenv {
directory = "./assets" //use ./ instead of /
filename = "env"
}

Is it possible to compress a prepackaged database for room persistence library?

I have a huge database of about 150mb. Can I put the compressed version of the database e.g. zip in the asset folder for room to use or is that not possible?
PS: android studio apk compression is not sufficient enough
First you need a function which can unzip archive to a some directory:
// unzip(new File("/sdcard/whatToUnzip.zip"), new File("/toThisFolder"));
fun unzip(zipFile: File, targetDirectory: File) {
unzip(BufferedInputStream(FileInputStream(zipFile)), targetDirectory)
}
fun unzip(zipInputStream: InputStream, targetDirectory: File) {
try {//BufferedInputStream(zipFileStream)
ZipInputStream(zipInputStream).use { zipInput ->
var zipEntry: ZipEntry
var count: Int
val buffer = ByteArray(65536)
while (zipInput.nextEntry.also { zipEntry = it } != null) {
val file = File(targetDirectory, zipEntry.name)
val dir: File? = if (zipEntry.isDirectory) file else file.parentFile
if (dir != null && !dir.isDirectory && !dir.mkdirs()) throw FileNotFoundException(
"Failed to ensure directory: " + dir.absolutePath
)
if (zipEntry.isDirectory) continue
FileOutputStream(file).use { fileOutput ->
while (zipInput.read(buffer).also { count = it } != -1) fileOutput.write(
buffer,
0,
count
)
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
I got it out of that stackoverflow's thread. Please read a thread to get more details. Then I added two method to work with a file from app's asset folder:
fun unzipAsset(assetsFilePath: String, context: Context, targetDirectory: File) {
unzip(context.assets.open(assetsFilePath), targetDirectory)
}
fun Context.unzipAsset(assetsFilePath: String, targetDirectory: File) = unzipAsset(
assetsFilePath,
this,
targetDirectory
)
Now we can unzip file to folder. To avoid copying an unzipped db file by room when I use createFromAsset or createFromFile methods of Room.databaseBuilder I want to unzip file to apps databases folder which used by room to store db file. That why I need additional methods to get db folder path and to check when db file already exist:
fun Context.databaseFolderPath(): File? = this.getDatabasePath("any.db").parentFile
// name – The name of the database file.
fun Context.isRoomDbFileExist(name: String): Boolean {
return this.getDatabasePath(name)?.exists() ?: false
}
And now, how to use all thinks together:
abstract class AppDatabase : RoomDatabase() {
companion object {
private const val DB_NAME = "sunflower-db"
// Create and pre-populate the database. See this article for more details:
// https://medium.com/google-developers/7-pro-tips-for-room-fbadea4bfbd1#4785
private fun buildDatabase(context: Context): AppDatabase {
if(!context.isRoomDbFileExist(DB_NAME)) {
// unzip db file to app's databases directory to avoid copy of unzipped file by room
context.unzipAsset("sunflower-db.zip", context.databaseFolderPath()!!)
// or unzip(File("your file"), context.databaseFolderPath()!!)
}
return Room.databaseBuilder(context, AppDatabase::class.java, DB_NAME)
//.createFromAsset(DB_NAME) // not zipped db file
.build()
}
}
}
I test this code on nice open source project - sunflower. Next I want to show screen with project structure , where sunflower-db.zip located:
The approach above works but You shouldn't take this sample as right or best solution. You should to think about avoid unzipping process from main thread. May be will be better if you implement your own SupportSQLiteOpenHelper.Factory(look like complicated).

Xamarin forms app throws a null exception when creating StandardKernel

I created with VS2017 a cross-platform App(Xamarin Forms) with the template set to Blank App, Platform Android, UI Tech Xamarin.Forms, Code Sharing .NET Standard. All builds and runs and displays "Welcome to Xamarin Froms".
I added the Portable.Ninject package to both the .NET standard PCL project and the Android project.
Created the following Test class and interface
public class Test : ITest
{
public string name { get; set; }
}
public interface ITest
{
string name { get; set; }
}
A NinjectModule class
public class modules : NinjectModule
{
public override void Load()
{
Bind<ITest>().To<Test>();
}
}
and in the class App:Application and added kernel creation in the constructor
public App ()
{
InitializeComponent();
var settings = new NinjectSettings();
settings.LoadExtensions = false;
var mods = new modules();
var kernel = new StandardKernel(mods);
var test = kernel.Get<ITest>();
MainPage = new App12.MainPage();
}
When ran an ArgumentNULLException. Parameter name:path1 occurs at the new StanardKernel( new modules() ).
Any help will be appreciated. I have tried the Ninject Nuget package, creating the StandardKernel in the android project MainActivity:Oncreate, deleting packages, bin and obj folders ... all with the same result
Addition :- Separated the instantiate of modules class the following is watch window. Shouldn't the Binding list have a count of 1 ?
- mods {App12.modules} App12.modules
- base {Ninject.Modules.NinjectModule} Ninject.Modules.NinjectModule
+ base {Ninject.Syntax.BindingRoot} Ninject.Syntax.BindingRoot
+ Bindings Count = 0 System.Collections.Generic.List<Ninject.Planning.Bindings.IBinding>
Kernel (null) Ninject.IKernel
Name "App12.modules" string
+ Non-public members
You should add a NinjaSettings object with LoadExtensions set to false to the StandardKernel constructor.
public App()
{
InitializeComponent();
var settings = new NinjectSettings();
settings.LoadExtensions = false;
var kernel = new StandardKernel(settings, new modules());
var test = kernel.Get<ITest>();
MainPage = new App12.MainPage();
}

Class.getResourceAsStream in Robolectric environment fails to return correct stream in M platform

I've got a problem with switching to M platform in robolectric runner.
Class.getResourceAsStream seems to perform differently when using Build.VERSION_CODES.M and Build.VERSION_CODES.LOLLIPOP_MR1
My application uses PhoneUtils library that loads its metadata using the following code:
static final MetadataLoader DEFAULT_METADATA_LOADER = new MetadataLoader() {
#Override
public InputStream loadMetadata(String metadataFileName) {
return MetadataManager.class.getResourceAsStream(metadataFileName);
}
};
When configuring test with:
#Config(
constants = BuildConfig::class,
sdk = intArrayOf(Build.VERSION_CODES.M)
)
resulting JarUrlConnection is set to:
sun.net.www.protocol.jar.JarURLConnection:jar:file:/C:/Users/motorro/.m2/repository/org/robolectric/android-all/6.0.0_r1-robolectric-0/android-all-6.0.0_r1-robolectric-0.jar!/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_US
(refers roboelectric platform mock) and fails to read a file.
If configured with:
#Config(
constants = BuildConfig::class,
sdk = intArrayOf(Build.VERSION_CODES.LOLLIPOP_MR1)
)
resulting JarUrlConnection is set to:
sun.net.www.protocol.jar.JarURLConnection:jar:file:/C:/Users/motorro/.gradle/caches/modules-2/files-2.1/com.googlecode.libphonenumber/libphonenumber/8.0.0/ce021971974ee6a26572e43eaba7edf184c3c63d/libphonenumber-8.0.0.jar!/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_US
which points to correct library file (test passes).
Here is the test:
#RunWith(RobolectricTestRunner::class)
class ExampleUnitTest {
companion object {
private val PHONE: Phonenumber.PhoneNumber
init {
PHONE = Phonenumber.PhoneNumber()
PHONE.countryCode = 7
PHONE.nationalNumber = 4956360636
}
}
#Test
#Config(
constants = BuildConfig::class,
sdk = intArrayOf(Build.VERSION_CODES.LOLLIPOP_MR1)
)
fun withLollipop() {
PhoneNumberUtil.getInstance().format(PHONE, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL)
}
#Test
#Config(
constants = BuildConfig::class,
sdk = intArrayOf(Build.VERSION_CODES.M)
)
fun withM() {
PhoneNumberUtil.getInstance().format(PHONE, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL)
}
}
Is it a roboelectric problem or do I miss some configuration here?
The duct-tape solution for now is to #Config all failing tests individually.
Current dependencies:
compile 'com.googlecode.libphonenumber:libphonenumber:8.0.0'
testCompile 'junit:junit:4.12'
testCompile "org.robolectric:robolectric:3.1.4"
Here is a whole test project for your convenience.
GitHub issue
Issue fixed in Robolectric 3.2.2

How to override the Robolectric runtime dependency repository URL?

We're trying to use the org.robolectric:robolectric:3.0 dependency from our own internal Nexus repository. The issue is that Robolectric tries to load some dependencies at runtime from a public repository (as mentioned here), and ignores any repository overrides in the build.gradle.
Since we don't have access to that public location from our intranet, my tests timeout after trying to load that dependency:
[WARNING] Unable to get resource
'org.robolectric:android-all:jar:5.0.0_r2-robolectric-1' from
repository sonatype (https://oss.sonatype.org/content/groups/public/):
Error transferring file: Operation timed out
The bottom section of the Robolectric configuration documentation recommends adding this to your Gradle configuration to override the URL:
android {
testOptions {
unitTests.all {
systemProperty 'robolectric.dependency.repo.url', 'https://local-mirror/repo'
systemProperty 'robolectric.dependency.repo.id', 'local'
}
}
}
Unfortunately, I've tested that and I never see that system property being set. I've printed it out from inside my custom Robolectric runner (which extends RobolectricGradleTestRunner) and that system property remains set to null.
System.out.println("robolectric.dependency.repo.url: " + System.getProperty("robolectric.dependency.repo.url"));
I also tried to do something similar to this comment (but that method doesn't exist to override in RobolectricGradleTestRunner), and I also tried setting the system properties directly in my custom Robolectric runner, and that didn't seem to help.
#Config(constants = BuildConfig.class)
public class CustomRobolectricRunner extends RobolectricGradleTestRunner {
private static final String BUILD_OUTPUT = "build/intermediates";
public CustomRobolectricRunner(Class<?> testClass) throws InitializationError {
super(testClass);
System.setProperty("robolectric.dependency.repo.url", "https://nexus.myinternaldomain.com/content");
System.setProperty("robolectric.dependency.repo.id", "internal");
System.out.println("robolectric.dependency.repo.url: " + System.getProperty("robolectric.dependency.repo.url"));
}
The Robolectric source code does seem to confirm that these system properties exist.
While not a fix for using the properties directly, another way to get this to work is by overriding getJarResolver() in a RobolectricTestRunner subclass and pointing it at your artifact host:
public final class MyTestRunner extends RobolectricTestRunner {
public MyTestRunner(Class<?> testClass) throws InitializationError {
super(testClass);
}
#Override protected DependencyResolver getJarResolver() {
return new CustomDependencyResolver();
}
static final class CustomDependencyResolver implements DependencyResolver {
private final Project project = new Project();
#Override public URL[] getLocalArtifactUrls(DependencyJar... dependencies) {
DependenciesTask dependenciesTask = new DependenciesTask();
RemoteRepository repository = new RemoteRepository();
repository.setUrl("https://my-nexus.example.com/content/groups/public");
repository.setId("my-nexus");
dependenciesTask.addConfiguredRemoteRepository(repository);
dependenciesTask.setProject(project);
for (DependencyJar dependencyJar : dependencies) {
Dependency dependency = new Dependency();
dependency.setArtifactId(dependencyJar.getArtifactId());
dependency.setGroupId(dependencyJar.getGroupId());
dependency.setType(dependencyJar.getType());
dependency.setVersion(dependencyJar.getVersion());
if (dependencyJar.getClassifier() != null) {
dependency.setClassifier(dependencyJar.getClassifier());
}
dependenciesTask.addDependency(dependency);
}
dependenciesTask.execute();
#SuppressWarnings("unchecked")
Hashtable<String, String> artifacts = project.getProperties();
URL[] urls = new URL[dependencies.length];
for (int i = 0; i < urls.length; i++) {
try {
urls[i] = Util.url(artifacts.get(key(dependencies[i])));
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
return urls;
}
#Override public URL getLocalArtifactUrl(DependencyJar dependency) {
URL[] urls = getLocalArtifactUrls(dependency);
if (urls.length > 0) {
return urls[0];
}
return null;
}
private String key(DependencyJar dependency) {
String key =
dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" + dependency.getType();
if (dependency.getClassifier() != null) {
key += ":" + dependency.getClassifier();
}
return key;
}
}
}
It should be noted that this relies on two internal classes of Robolectric so care should be taken when upgrading versions.
You can set properties mavenRepositoryId and mavenRepositoryUrl of RoboSettings which are used by MavenDependencyResolver.
Example:
public class CustomRobolectricRunner extends RobolectricGradleTestRunner {
static {
RoboSettings.setMavenRepositoryId("my-nexus");
RoboSettings.setMavenRepositoryUrl("https://my-nexus.example.com/content/groups/public");
}
...
}
As per the linked Github issue, one fix is to configure a settings.xml in your ~\.m2 folder:
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<mirrors>
<mirror>
<id>jcenter</id>
<name>JCenter Remote</name>
<mirrorOf>*</mirrorOf>
<url>https://www.example.com/artifactory/jcenter-remote/</url>
</mirror>
</mirrors>
</settings>
<mirrorOf>*</mirrorOf> seems necessary to force Maven to redirect all repository requests to the one remote. See here for more details about mirror settings in Maven.
I found that using a remote of Sonatype is not sufficient, you should use a remote of JCenter or Maven Central in order to obtain all of the transitive dependencies.
As of time of this writing, those previous answers are now obsolete. If you refer to the latest robolectric documentation you need to override the robolectric.dependency.repo.url property like so:
android {
testOptions {
unitTests.all {
systemProperty 'robolectric.dependency.repo.url', 'https://local-mirror/repo'
systemProperty 'robolectric.dependency.repo.id', 'local'
}
}
}

Categories

Resources