Skip to content

Commit

Permalink
Add doc blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
niranjan94 committed Dec 22, 2018
1 parent bfebe68 commit 8d3a751
Show file tree
Hide file tree
Showing 20 changed files with 173 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,16 @@ import com.njlabs.showjava.R
import com.njlabs.showjava.activities.BaseActivity
import kotlinx.android.synthetic.main.activity_about.*

/**
* Show information about the app, its version & licenses to all open source libraries used
*/
class AboutActivity : BaseActivity() {
override fun init(savedInstanceState: Bundle?) {
setupLayout(R.layout.activity_about)
if (BuildConfig.GIT_SHA.isNotEmpty()) {
version.setText(R.string.appVersionExtendedWithHash)
}

viewOpenSourceLicenses.setOnClickListener {
Crashlytics.getInstance().crash()
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ import io.reactivex.ObservableEmitter

class AppsHandler(private var context: Context) {

/**
* Load all installed applications.
*
* @return [Observable] which can be used to track the loading progress and completion state.
*/
fun loadApps(withSystemApps: Boolean): Observable<ProcessStatus<ArrayList<PackageInfo>>> {
return Observable.create { emitter: ObservableEmitter<ProcessStatus<ArrayList<PackageInfo>>> ->
val installedApps = ArrayList<PackageInfo>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.activity_low_memory.*
import timber.log.Timber

/**
* If an app's decompilation was stopped due to low memory, explain what happened to the user
* And also provide user a way to report the app that failed to decompile. This can then
* be investigated later on to see what can be done to reduce the memory usage.
*/
class LowMemoryActivity : BaseActivity() {

override fun init(savedInstanceState: Bundle?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,11 +219,10 @@ class NavigatorActivity : BaseActivity() {
}
}

/**
* Check if the current folder the user is in, is the root
*/
private fun isAtRoot(): Boolean {

Timber.d("[isAtRoot] currentDirectory: ${currentDirectory?.canonicalPath}")
Timber.d("[isAtRoot] sourceDirectory: ${selectedApp?.sourceDirectory?.canonicalPath}")

return currentDirectory?.canonicalPath == selectedApp?.sourceDirectory?.canonicalPath
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,14 @@ import kotlin.collections.sortBy

class NavigatorHandler(private var context: Context) {

fun loadFiles(currentFile: File): Observable<ArrayList<FileItem>> {
/**
* Load all files in the given directory
*/
fun loadFiles(currentDirectory: File): Observable<ArrayList<FileItem>> {
return Observable.fromCallable {
val directories = ArrayList<FileItem>()
val files = ArrayList<FileItem>()
val items = currentFile.listFiles()
val items = currentDirectory.listFiles()
if (items.isNullOrEmpty()) {
return@fromCallable directories
}
Expand Down Expand Up @@ -66,13 +69,19 @@ class NavigatorHandler(private var context: Context) {
}
}

/**
* Package an entire directory containing the source code into a .zip archive.
*/
fun archiveDirectory(sourceDirectory: File, packageName: String): Observable<File> {
return Observable.fromCallable {
ZipUtils.zipDir(sourceDirectory, packageName)
}
}

fun deleteDirectory(sourceDirectory: File): Observable<Any> {
/**
* Delete the source directory
*/
fun deleteDirectory(sourceDirectory: File): Observable<Unit> {
return Observable.fromCallable {
try {
if (sourceDirectory.exists()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import com.njlabs.showjava.R
import com.njlabs.showjava.data.FileItem
import kotlinx.android.synthetic.main.layout_app_list_item.view.*

/**
* List adapter for the code navigator
*/
class FilesListAdapter(
private var fileItems: List<FileItem>,
private val itemClick: (FileItem) -> Unit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import com.njlabs.showjava.utils.appStorage
import io.reactivex.Observable

class SettingsHandler(private var context: Context) {

/**
* Delete all decompiled sources recursively
*/
fun deleteHistory(): Observable<Any> {
return Observable.fromCallable {
appStorage.resolve("sources")
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/kotlin/com/njlabs/showjava/data/SourceInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ import timber.log.Timber
import java.io.File
import java.io.IOException

/**
* [SourceInfo] holds information about a specific decompiled source on the app storage.
* The source info is persisted to disk as a simple json file at the root of each package's
* decompiled source folder.
*/
class SourceInfo() : Parcelable {

lateinit var packageLabel: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ import java.io.File
import java.io.PrintStream
import java.util.concurrent.TimeUnit

/**
* The base decompiler. This reads the input [Data] into easy to use properties of the class.
* All other components of the decompiler will extend this one.
*/
abstract class BaseDecompiler(val context: Context, val data: Data) {
var printStream: PrintStream? = null

Expand Down Expand Up @@ -149,6 +153,9 @@ abstract class BaseDecompiler(val context: Context, val data: Data) {
this.broadcastStatus(null, message)
}

/**
* Set the current decompilation step
*/
fun setStep(title: String) {
sendStatus(title, context.getString(R.string.initializing), true)
}
Expand All @@ -166,6 +173,9 @@ abstract class BaseDecompiler(val context: Context, val data: Data) {
ListenableWorker.Result.RETRY
}

/**
* Return a success only if the conditions is true. Else exit with an exception.
*/
protected fun successIf(condition: Boolean): ListenableWorker.Result {
disposables.clear()
return if (condition)
Expand Down Expand Up @@ -202,6 +212,9 @@ abstract class BaseDecompiler(val context: Context, val data: Data) {
)
}

/**
* Clear notifications and show a success notification.
*/
fun onCompleted() {
processNotifier?.success()
broadcastStatus(
Expand All @@ -224,6 +237,9 @@ abstract class BaseDecompiler(val context: Context, val data: Data) {

companion object {

/**
* Check if the specified decompiler is available on the device based on the android version
*/
fun isAvailable(decompiler: String): Boolean {
return when (decompiler) {
"cfr" -> true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,22 @@ import java.io.BufferedInputStream
import java.io.InputStream
import java.util.zip.ZipFile


/**
* The [JarExtractionWorker] worker handles optimization and extraction of the jar/dex file from the source
*
* a. For APKs & Dex files:
* 1. All the classes within the input are read
* 2. Classes are checked against the ignore list.
* 3. Multiple dex files are created with the classes that are not ignored.
* 4. For inputs with a large number of classes, creating a single dex file causes devices to
* run out of memory. So, we chunk them to smaller dex files containing lesser number
* of classes.
* 5. The dex files are then converted to jar. (With the exception of JaDX which prefers dex files)
*
* b. For Jar files:
* 1. They are directly passed to the next step. (With the exception of JaDX where the jar is
* read and converted into dex chunks and then passed on to JaDX)
*/
class JarExtractionWorker(context: Context, data: Data) : BaseDecompiler(context, data) {

private var ignoredLibs: ArrayList<String> = ArrayList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,21 @@ import timber.log.Timber
import java.io.File
import java.io.FileNotFoundException


/**
* The [JavaExtractionWorker] does the actual decompilation of extracting the `java` source from
* the inputs. All of the three decompiler that we support, allow passing in multiple input files.
* So, we pass in all of the chunks of either dex (in case of JaDX) or jar files (in case of CFR &
* fernflower) as the input.
*/
class JavaExtractionWorker(context: Context, data: Data) : BaseDecompiler(context, data) {

/**
* Do the decompilation using the CFR decompiler.
*
* We set the `lowmem` flag as true to let CFR know that it has to be more aggressive in terms
* of garbage collection and less-aggressive caching. This results in reduced performance. But
* increase success rates for large inputs. Which is a good trade-off.
*/
@Throws(Exception::class)
private fun decompileWithCFR(jarInputFiles: File, javaOutputDir: File) {
cleanMemory()
Expand All @@ -49,6 +61,12 @@ class JavaExtractionWorker(context: Context, data: Data) : BaseDecompiler(contex
cfrDriver.analyse(jarFiles.map { it.canonicalPath })
}

/**
* Do the decompilation using the JaDX decompiler.
*
* We set `threadsCount` as 1. This instructs JaDX to not spawn additional threads to prevent
* issues on some devices.
*/
@Throws(Exception::class)
private fun decompileWithJaDX(dexInputFiles: File, javaOutputDir: File) {
cleanMemory()
Expand All @@ -66,6 +84,12 @@ class JavaExtractionWorker(context: Context, data: Data) : BaseDecompiler(contex
}
}

/**
* Do the decompilation using FernFlower decompiler.
*
* The out of the decompiler is a jar archive containing the decompiled java files. So, we look
* for and extract the archive after the decompilation.
*/
@Throws(Exception::class)
private fun decompileWithFernFlower(jarInputFiles: File, javaOutputDir: File) {
cleanMemory()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ class ResourcesExtractionWorker(context: Context, data: Data) : BaseDecompiler(c
private lateinit var parsedInputApkFile: ApkFile
private val images = listOf("jpg", "png", "gif", "jpeg", "webp", "tiff", "bmp")

/**
* Extract xml & image resources using JaDX. JaDX has a better resources decompiler compared
* to others. But sadly, JaDX also uses javax.imageio classes that are not available in android
* at present. So, we don't use it.
*
* TODO Figure out how to use it only for XML though.
*
* @experimental
*/
@Throws(Exception::class)
private fun extractResourcesWithJadx() {
cleanMemory()
Expand All @@ -65,6 +74,10 @@ class ResourcesExtractionWorker(context: Context, data: Data) : BaseDecompiler(c
jadx.saveResources()
}

/**
* Read the APK as zip, and extract XML resources using the apk-parser and image/other resources
* by just extracting it from the zip.
*/
@Throws(Exception::class)
private fun extractResourcesWithParser() {
cleanMemory()
Expand All @@ -90,6 +103,13 @@ class ResourcesExtractionWorker(context: Context, data: Data) : BaseDecompiler(c
zipFile.close()
}

/**
* Currently the extracted XML resources, refer to the resource used within them via their
* numeric ID. This is an experiment to parse the resource table in the APK and get the human
* readable names from the numeric ID.
*
* @experimental
*/
@Suppress("UNCHECKED_CAST")
@RequiresApi(Build.VERSION_CODES.N)
@Throws(Exception::class)
Expand All @@ -111,6 +131,9 @@ class ResourcesExtractionWorker(context: Context, data: Data) : BaseDecompiler(c
}
}

/**
* Write a file at the appropriate output path within the source directory
*/
@Throws(Exception::class)
private fun writeFile(fileStream: InputStream, path: String) {
val fileFolderPath =
Expand All @@ -126,6 +149,9 @@ class ResourcesExtractionWorker(context: Context, data: Data) : BaseDecompiler(c
fileStream.toFile(File(fileFolderPath, FilenameUtils.getName(path)))
}

/**
* Read and decompile an XML resource from the APK and write it to the source directory.
*/
@Throws(ParserException::class)
private fun writeXML(path: String) {
val xml = parsedInputApkFile.transBinaryXml(path)
Expand All @@ -146,6 +172,9 @@ class ResourcesExtractionWorker(context: Context, data: Data) : BaseDecompiler(c
)
}

/**
* Write the AndroidManifest file to the source directory
*/
@Throws(Exception::class)
private fun writeManifest() {
val manifestXml = parsedInputApkFile.manifestXml
Expand All @@ -156,6 +185,9 @@ class ResourcesExtractionWorker(context: Context, data: Data) : BaseDecompiler(c
)
}

/**
* Get bitmap from drawable. This is used to read the app icon and save to the source directory.
*/
// Borrowed from from https://stackoverflow.com/a/52453231/1562480
private fun getBitmapFromDrawable(drawable: Drawable): Bitmap {
val bmp = Bitmap.createBitmap(
Expand All @@ -169,6 +201,9 @@ class ResourcesExtractionWorker(context: Context, data: Data) : BaseDecompiler(c
return bmp
}

/**
* Save the app icon to the source directory
*/
@Throws(Exception::class)
private fun saveIcon() {
val packageInfo = context.packageManager.getPackageArchiveInfo(inputPackageFile.canonicalPath, 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ import com.njlabs.showjava.Constants
import com.njlabs.showjava.workers.DecompilerWorker
import timber.log.Timber

/**
* [DecompilerActionReceiver] is used to receive the cancel request from the notification action,
* and cancel the decompilation process.
*/
class DecompilerActionReceiver : BroadcastReceiver() {

override fun onReceive(context: Context?, intent: Intent?) {
Expand Down
8 changes: 7 additions & 1 deletion app/src/main/kotlin/com/njlabs/showjava/utils/Ads.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ import com.njlabs.showjava.activities.purchase.PurchaseActivity
import timber.log.Timber
import java.net.URL


/**
* Initialize the ads library. Also, takes care of showing a consent screen for users within EU
* and persisting the consent.
*/
class Ads(val context: Context) {
private val consentInformation: ConsentInformation = ConsentInformation.getInstance(context)
private lateinit var consentForm: ConsentForm
Expand Down Expand Up @@ -58,6 +61,9 @@ class Ads(val context: Context) {
})
}

/**
* Load the consent screen and prepare to display.
*/
fun loadConsentScreen(): ConsentForm? {
consentForm = ConsentForm.Builder(context, URL(context.getString(R.string.privacyPolicyUrl)))
.withListener(object : ConsentFormListener() {
Expand Down
Loading

0 comments on commit 8d3a751

Please sign in to comment.