Build an APK Extractor Android App using Kotlin

35 minutes
10 months ago
<h2><b>Overview</b></h2><p>An APK Extractor app lets you manage the apps in your phone, providing you with features to extract the apk out of any app, share the apk using other apps and uninstall the apps from your phone. In this tutorial we will be going hands on to build the app using Kotlin.</p><h2><b>Prerequisites</b></h2><p>Basic knowledge of android app development, Kotlin and willpower to handle me till the end of this article.</p><p>So let’s dive right in and start building the app. We’ll learn the necessary topics on the go.</p><p>As our app will extract APKs so lets call it APK Extractor (Such creativity, much wow ¯\_(ツ)_/¯).</p><h2><b>Source Code</b></h2><p>You can find the source code for this app here: https://github.com/rajdeep1008/ApkExtractor</p><h2><b>Code</b></h2><h3><b>Creating the project</b></h3><p>Create a new android studio project with the name APK Extractor. Select the Kotlin support checkbox -&gt; Select desired API level -&gt; Select Empty Activity and finish. This will create a basic android studio project with MainActivity.kt and activity_main.xml files.</p><p><img src="https://i.imgur.com/afW8g3k.png"><br></p><h3><b>Adding the dependencies</b></h3><p>Open up the app level build.gradle file and add the following dependencies :</p><pre>apply plugin: 'com.android.application'<br>apply plugin: 'kotlin-android'<br>apply plugin: 'kotlin-android-extensions'<br>android {<br> compileSdkVersion 27<br> defaultConfig {<br> applicationId "io.github.rajdeep1008.apkextractor"<br> minSdkVersion 19<br> targetSdkVersion 27<br> versionCode 1<br> versionName "1.0"<br> testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"<br> }<br> buildTypes {<br> release {<br> minifyEnabled false<br> proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'<br> }<br> }<br>}<br>dependencies {<br> implementation fileTree(dir: 'libs', include: ['*.jar'])<br> implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.51'<br> implementation "org.jetbrains.anko:anko-common:0.10.5"<br> implementation 'com.android.support:appcompat-v7:27.1.1'<br> implementation 'commons-io:commons-io:2.4'<br> implementation 'com.android.support:design:27.1.1'<br> implementation 'com.android.support:cardview-v7:27.1.1'<br>}</pre><p>We will be using the anko library to make our asynctask work easier so as to load the apk details on a background thread. Apache io commons will be used to ease some file related functions, and design and cardview support library are for UI purposes.</p><h3><b>Configuring the Manifest file</b></h3><pre>To save our extracted apk, we are going to need WRITE_EXTERNAL_STORAGE permission. So let’s add it quickly :<br>&lt;?xml version="1.0" encoding="utf-8"?&gt;<br>&lt;manifest xmlns:android="http://schemas.android.com/apk/res/android"<br> package="io.github.rajdeep1008.apkextractor"&gt;<br> &lt;uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/&gt;<br> &lt;application<br> android:allowBackup="true"<br> android:icon="@mipmap/ic_launcher"<br> android:label="@string/app_name"<br> android:roundIcon="@mipmap/ic_launcher_round"<br> android:supportsRtl="true"<br> android:theme="@style/AppTheme"&gt;<br> &lt;activity android:name=".MainActivity"&gt;<br> &lt;intent-filter&gt;<br> &lt;action android:name="android.intent.action.MAIN" /&gt;<br> &lt;category android:name="android.intent.category.LAUNCHER" /&gt;<br> &lt;/intent-filter&gt;<br> &lt;/activity&gt;<br> &lt;/application&gt;<br>&lt;/manifest&gt;</pre><p>We are going to request this permission at runtime too as this falls into the dangerous permissions list starting from Android 6.0 and higher.</p><h3><b>Creating the User Interface</b></h3><p>Let’s start by creating the user interface for our app. Our app will consist of a simple list showing all the apps currently installed in our phone with 5 options:</p><p>1. Extract the apk from the app to a specified directory on our phone.</p><p>2. Share the apk using other apps or save it to dropbox, google drive, etc.</p><p>3. Uninstall the app.</p><p>4. Open the app in playstore.</p><p>5. Launch the app.</p><p>This is what our app will look like.</p><p><img src="https://i.imgur.com/DhlVGyj.png"><br></p><p><br></p><p>To show our installed apps on screen, we are going to use a Recyclerview in a Relative layout along with a Progressbar to show loading icon while the APKs are being fetched.</p><p>Our activity_main.xml looks like this now :</p><pre>&lt;RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"<br> xmlns:tools="http://schemas.android.com/tools"<br> android:layout_width="match_parent"<br> android:layout_height="match_parent"&gt;<br> &lt;android.support.v7.widget.RecyclerView<br> android:id="@+id/apk_list_rv"<br> android:layout_width="match_parent"<br> android:layout_height="wrap_content"<br> android:layout_marginTop="8dp"<br> tools:listitem="@layout/layout_apk_item" /&gt;<br> &lt;ProgressBar<br> android:id="@+id/progress"<br> android:layout_width="36dp"<br> android:layout_height="36dp"<br> android:layout_centerInParent="true"<br> android:indeterminateTint="@color/colorPrimary"<br> android:visibility="visible" /&gt;<br>&lt;/RelativeLayout&gt;</pre><p><br></p><p>Next, we are going to define how a single item in our Recyclerview will look like. Create a xml file and name it layout_apk_item.xml:<br></p><pre>&lt;?xml version="1.0" encoding="utf-8"?&gt;<br>&lt;RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"<br> xmlns:tools="http://schemas.android.com/tools"<br> android:layout_width="match_parent"<br> android:layout_height="wrap_content"&gt;<br> &lt;android.support.v7.widget.CardView<br> android:layout_width="match_parent"<br> android:layout_height="120dp"<br> android:layout_margin="8dp"&gt;<br> &lt;RelativeLayout<br> android:id="@+id/container"<br> android:layout_width="match_parent"<br> android:layout_height="match_parent"&gt;<br> &lt;ImageView<br> android:id="@+id/apk_icon_iv"<br> android:layout_width="48dp"<br> android:layout_height="48dp"<br> android:layout_marginStart="16dp"<br> android:layout_marginTop="12dp"<br> android:maxHeight="48dp"<br> android:maxWidth="48dp"<br> tools:src="@color/colorPrimary" /&gt;<br> &lt;TextView<br> android:id="@+id/apk_label_tv"<br> android:layout_width="wrap_content"<br> android:layout_height="wrap_content"<br> android:layout_alignTop="@+id/apk_icon_iv"<br> android:layout_marginStart="16dp"<br> android:layout_toEndOf="@+id/apk_icon_iv"<br> android:layout_toStartOf="@+id/menu_btn"<br> android:ellipsize="end"<br> android:fontFamily="sans-serif-light"<br> android:maxLines="1"<br> android:text="Dummy Label"<br> android:textColor="@android:color/black"<br> android:textSize="20sp" /&gt;<br> &lt;TextView<br> android:id="@+id/apk_package_tv"<br> android:layout_width="wrap_content"<br> android:layout_height="wrap_content"<br> android:layout_below="@+id/apk_label_tv"<br> android:layout_marginStart="16dp"<br> android:layout_toEndOf="@+id/apk_icon_iv"<br> android:layout_toStartOf="@+id/menu_btn"<br> android:ellipsize="end"<br> android:fontFamily="sans-serif-thin"<br> android:maxLines="1"<br> android:text="Dummy Package"<br> android:textColor="@android:color/black"<br> android:textSize="14sp" /&gt;<br> &lt;ImageButton<br> android:id="@+id/menu_btn"<br> android:layout_width="wrap_content"<br> android:layout_height="wrap_content"<br> android:layout_alignParentEnd="true"<br> android:layout_marginEnd="8dp"<br> android:layout_marginRight="8dp"<br> android:layout_marginTop="12dp"<br> android:background="@android:color/transparent"<br> android:src="@drawable/menu_btn"<br> android:tint="@color/colorPrimaryDark" /&gt;<br> &lt;LinearLayout<br> android:layout_width="match_parent"<br> android:layout_height="wrap_content"<br> android:layout_alignParentBottom="true"<br> android:orientation="horizontal"<br> android:weightSum="3"&gt;<br> &lt;Button<br> android:id="@+id/extract_btn"<br> style="?android:attr/borderlessButtonStyle"<br> android:layout_width="0dp"<br> android:layout_height="wrap_content"<br> android:layout_weight="1"<br> android:text="Extract"<br> android:textColor="@color/colorPrimary"<br> android:textSize="14sp" /&gt;<br> &lt;Button<br> android:id="@+id/share_btn"<br> style="?android:attr/borderlessButtonStyle"<br> android:layout_width="0dp"<br> android:layout_height="wrap_content"<br> android:layout_weight="1"<br> android:text="Share Apk"<br> android:textColor="@color/colorPrimary"<br> android:textSize="14sp" /&gt;<br> &lt;Button<br> android:id="@+id/uninstall_btn"<br> style="?android:attr/borderlessButtonStyle"<br> android:layout_width="0dp"<br> android:layout_height="wrap_content"<br> android:layout_weight="1"<br> android:text="Uninstall"<br> android:textColor="@color/colorPrimary"<br> android:textSize="14sp" /&gt;<br> &lt;/LinearLayout&gt;<br> &lt;/RelativeLayout&gt;<br> &lt;/android.support.v7.widget.CardView&gt;<br>&lt;/RelativeLayout&gt;</pre><h3><b>Writing the Kotlin part</b></h3><p>Now with our UI parts set up, let’s start with the coding part.</p><p>First of all, make a new package named models and add a Kotlin file called Apk.kt. This is a data class and works like a POJO(Plain Old Java Object) to hold the required information about our apk object. In case you are unfamiliar with data classes in Kotlin, have a look here[ https://kotlinlang.org/docs/reference/data-classes.html ].</p><pre>package io.github.rajdeep1008.models<br>import android.content.pm.ApplicationInfo<br>data class Apk(val appInfo: ApplicationInfo,<br> val appName: String,<br> val packageName: String? = "",<br> val version: String? = "")</pre><p><br></p><p>It has a constructor with 4 parameters, appInfo of type ApplicationInfo which consist of all the details about each of the application installed, while appName, packageName and version are Strings to ease up our work so we don’t have to extract them again and again.</p><p>Next up is our adapter class for recyclerview. Create a package with the name adapter and create ApkListAdapter.kt class which extends RecyclerView.Adapter and accept 2 parameters-i.e., an arraylist of our Apk objects and a context. It consists of an interface which our MainActivity will implement so as to get the click callbacks with the correct package name.</p><pre>class ApkListAdapter(var apkList: ArrayList&lt;Apk&gt;, val context: Context) : RecyclerView.Adapter&lt;ApkListAdapter.ApkListViewHolder&gt;() {<br> var mItemClickListener: OnContextItemClickListener? = null<br> init {<br> mItemClickListener = context as MainActivity<br> }<br> override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ApkListViewHolder {<br> return ApkListViewHolder(LayoutInflater.from(context).inflate(R.layout.layout_apk_item, parent, false), context, apkList)<br> }<br> override fun onBindViewHolder(holder: ApkListViewHolder, position: Int) {<br> holder.mIconImageView.setImageDrawable(context.packageManager.getApplicationIcon(apkList.get(position).appInfo))<br> holder.mLabelTextView.text = context.packageManager.getApplicationLabel(apkList.get(position).appInfo).toString()<br> holder.mPackageTextView.text = apkList.get(position).packageName<br> }<br> override fun getItemCount(): Int {<br> return apkList.size<br> }<br> inner class ApkListViewHolder(view: View, context: Context, apkList: ArrayList&lt;Apk&gt;) : RecyclerView.ViewHolder(view) {<br> val mIconImageView: ImageView = view.find(R.id.apk_icon_iv)<br> val mLabelTextView: TextView = view.find(R.id.apk_label_tv)<br> val mPackageTextView: TextView = view.find(R.id.apk_package_tv)<br> private val mExtractBtn: Button = view.find(R.id.extract_btn)<br> private val mShareBtn: Button = view.find(R.id.share_btn)<br> private val mUninstallBtn: Button = view.find(R.id.uninstall_btn)<br> private val mMenuBtn: ImageButton = view.find(R.id.menu_btn)<br> init {<br> (context as Activity).registerForContextMenu(mMenuBtn)<br> mExtractBtn.setOnClickListener {<br> if (Utilities.checkPermission(context as MainActivity)) {<br> Utilities.extractApk(apkList.get(adapterPosition))<br> val rootView: View = context.window.decorView.findViewById(android.R.id.content)<br> Snackbar.make(rootView, "${apkList.get(adapterPosition).appName} apk extracted successfully", Snackbar.LENGTH_LONG).show()<br> }<br> }<br> mShareBtn.setOnClickListener {<br> if (Utilities.checkPermission(context as MainActivity)) {<br> val intent = Utilities.getShareableIntent(apkList.get(adapterPosition))<br> context.startActivity(Intent.createChooser(intent, "Share the apk using"))<br> }<br> }<br> mUninstallBtn.setOnClickListener {<br> val uninstallIntent = Intent(Intent.ACTION_UNINSTALL_PACKAGE)<br> uninstallIntent.data = Uri.parse("package:" + apkList[adapterPosition].packageName);<br> uninstallIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true)<br> context.startActivity(uninstallIntent)<br> }<br> mMenuBtn.setOnClickListener {<br> mItemClickListener?.onItemClicked(apkList.get(adapterPosition).packageName!!)<br> context.openContextMenu(mMenuBtn)<br> mMenuBtn.setOnCreateContextMenuListener { menu, _, _ -&gt;<br> context.menuInflater.inflate(R.menu.context_menu, menu)<br> }<br> }<br> }<br> }<br> interface OnContextItemClickListener {<br> fun onItemClicked(packageName: String)<br> }</pre><p><br></p><p>onCreateViewHolder and onBindViewHolder methods create and set up viewholders for each of our item in the arraylist and inflates our earlier created xml for item layout.</p><p>Our adapter also holds an inner class ApkListViewHolder which extends from RecyclerView.ViewHolder. It binds up the click listeners for our extract, share, uninstall and menu buttons.</p><p>The functions to extract the apk and request permissions at runtime are defined in a class called Utilities.kt in a package called extras.</p><pre>package io.github.rajdeep1008.extras<br>class Utilities {<br> companion object {<br> val STORAGE_PERMISSION_CODE = 1008<br> fun checkPermission(activity: AppCompatActivity): Boolean {<br> var permissionGranted = false<br> if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {<br> if (ActivityCompat.shouldShowRequestPermissionRationale(activity,<br> Manifest.permission.WRITE_EXTERNAL_STORAGE)) {<br> val rootView: View = (activity as MainActivity).window.decorView.findViewById(android.R.id.content)<br> Snackbar.make(rootView, "Storage permission required", Snackbar.LENGTH_LONG)<br> .setAction("Allow") {<br> ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), STORAGE_PERMISSION_CODE)<br> }<br> .setActionTextColor(Color.WHITE)<br> .show()<br> } else {<br> ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), STORAGE_PERMISSION_CODE)<br> }<br> } else {<br> permissionGranted = true<br> }<br> return permissionGranted<br> }<br> fun checkExternalStorage(): Boolean {<br> return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)<br> }<br> fun getAppFolder(): File? {<br> var file: File? = null<br> if (checkExternalStorage()) {<br> file = File(Environment.getExternalStorageDirectory(), "ApkExtractor")<br> return file<br> }<br> return file<br> }<br> fun makeAppDir() {<br> val file = getAppFolder()<br> if (file != null &amp;&amp; !file.exists()) {<br> file.mkdir()<br> }<br> }<br> fun extractApk(apk: Apk): Boolean {<br> makeAppDir()<br> var extracted = true<br> val originalFile = File(apk.appInfo.sourceDir)<br> val extractedFile: File = getApkFile(apk)<br> try {<br> FileUtils.copyFile(originalFile, extractedFile)<br> extracted = true<br> } catch (e: Exception) {<br> Log.d("test", "problem - " + e.message)<br> }<br> return extracted<br> }<br> fun getApkFile(apk: Apk): File {<br> var fileName = getAppFolder()?.path + File.separator + apk.appName + "_" + apk.version + ".apk"<br> return File(fileName)<br> }<br> fun getShareableIntent(apk: Apk): Intent {<br> extractApk(apk)<br> val file = getApkFile(apk)<br> var shareIntent = Intent()<br> shareIntent.setAction(Intent.ACTION_SEND)<br> shareIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file))<br> shareIntent.setType("application/vnd.android.package-archive")<br> shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)<br> return shareIntent<br> }<br> }<br>}</pre><p><br></p><p>The checkPermission method will handle the permissions for writing to external storage and alert the user in case the user denies it. makeAppDir method will make a directory with the name “ApkExtractor” in the external storage. extractApk method creates a File object from the sourceDir variable of ApplicationInfo in our Apk object and copy the apk file to our newly created app directory from where the users can easily find all the extracted apks. getShareableIntent method creates an Intent with the extracted apk URI and handles the Action and Extras part.<br></p><p>Just hang on a little more, we are almost done with the app. Just one last file called MainActivity which uses all the previously created Kotlin files. MainActivity implements the ApkListAdapter.OnContextItemClickListener which we created earlier to handle the clicked item.</p><p>In the onCreate method, we set up the recyclerview, progressbar and our ApkListAdapter. Utilities.checkPermission() method is also called in the onCreate to ask the user for necessary permissions.</p><pre>package io.github.rajdeep1008.apkextractor<br>class MainActivity : AppCompatActivity(), ApkListAdapter.OnContextItemClickListener {<br> private lateinit var progressBar: ProgressBar<br> private val apkList = ArrayList&lt;Apk&gt;()<br> private lateinit var contextItemPackageName: String<br> private lateinit var mAdapter: ApkListAdapter<br> private lateinit var mLinearLayoutManager: LinearLayoutManager<br> lateinit var mRecyclerView: RecyclerView<br> override fun onCreate(savedInstanceState: Bundle?) {<br> super.onCreate(savedInstanceState)<br> setContentView(R.layout.activity_main)<br> progressBar = find(R.id.progress)<br> Utilities.checkPermission(this)<br> mRecyclerView = find(R.id.apk_list_rv)<br> mLinearLayoutManager = LinearLayoutManager(this)<br> mAdapter = ApkListAdapter(apkList, this)<br> mRecyclerView.layoutManager = mLinearLayoutManager<br> mRecyclerView.adapter = mAdapter<br> loadApk()<br> }<br> private fun loadApk() {<br> doAsync {<br> val allPackages: List&lt;PackageInfo&gt; = packageManager.getInstalledPackages(PackageManager.GET_META_DATA)<br> allPackages.forEach {<br> val applicationInfo: ApplicationInfo = it.applicationInfo<br> val userApk = Apk(<br> applicationInfo,<br> packageManager.getApplicationLabel(applicationInfo).toString(),<br> it.packageName,<br> it.versionName)<br> apkList.add(userApk)<br> }<br> uiThread {<br> mAdapter.notifyDataSetChanged()<br> progressBar.visibility = View.GONE<br> }<br> }<br> }<br> override fun onRequestPermissionsResult(requestCode: Int, permissions: Array&lt;out String&gt;, grantResults: IntArray) {<br> when (requestCode) {<br> Utilities.STORAGE_PERMISSION_CODE -&gt; {<br> if ((grantResults.isNotEmpty() &amp;&amp; grantResults[0] == PackageManager.PERMISSION_GRANTED)) {<br> Utilities.makeAppDir()<br> } else {<br> Snackbar.make(find(android.R.id.content), "Permission required to extract apk", Snackbar.LENGTH_LONG).show()<br> }<br> }<br> }<br> }<br> override fun onContextItemSelected(item: MenuItem?): Boolean {<br> when (item?.itemId) {<br> R.id.action_launch -&gt; {<br> try {<br> startActivity(packageManager.getLaunchIntentForPackage(contextItemPackageName))<br> } catch (e: Exception) {<br> Snackbar.make(find(android.R.id.content), "Can't open this app", Snackbar.LENGTH_SHORT).show()<br> }<br> }<br> R.id.action_playstore -&gt; {<br> try {<br> startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$contextItemPackageName")))<br> } catch (e: ActivityNotFoundException) {<br> startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=$contextItemPackageName")))<br> }<br> }<br> }<br> return true<br> }<br> override fun onItemClicked(packageName: String) {<br> contextItemPackageName = packageName<br> }<br>}</pre><p><br></p><p>The method <b>loadApk()</b> consists of the main functionality of our app. We call <b>getInstalledPackages</b> on the package manager to get the list of installed apps and put them up in out Apk arraylist. All this is done on the background thread using the <b>doAsync</b> from Anko library. Once the process is done, we notify the adapter and hide the progressbar.<br></p><h2><b>Conclusion</b></h2><p>With this, your Apk extractor app is ready. Of course this app can be improved a lot by adding features such as automatically refreshing the apk list after uninstalling any app or dividing the list into system and user installed apps and so on. Feel free to work upon the code and create amazing apps out of it.</p><p>You can find a better version on which i am working currently here[ https://github.com/rajdeep1008/ApkWizard ]</p>

Comments

You must login to comment