paint-brush
How to Build Your Own App Storeby@adtechholding
1,133 reads
1,133 reads

How to Build Your Own App Store

by AdTech HoldingOctober 24th, 2022
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

AdTech Holding is committed to high technologies that ensure top-notch quality for all its products. Our AppLab team is responsible for developing mobile apps, but recently they have gone beyond the limits and created their own app store platform. Artem Kovardin, the Head of Product Development of App Lab, shared the best practices from his team’s experience and we are ready to bring out a step-by-step guide including all necessary tools. The guide includes a guide on how to build a store platform using Flutter and the Go Language.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - How to Build Your Own App Store
AdTech Holding HackerNoon profile picture

AdTech Holding is committed to high technologies that ensure top-notch quality for all its products. Our business approach relies much on software development and thus implies having a team of strong IT specialists who have not only enormous expertise but also an inspiration to grow and create. We always encourage our tech teams to go beyond their normal daily, quarterly, and annual tasks — and it turns into brand new ideas and insights.


Today, we are glad to share one of the great examples of such an insight. Our AppLab team is responsible for developing mobile apps, but recently they have gone beyond the limits and created their own app store platform. We spoke to Artem Kovardin, the Head of Product Development of App Lab, to get the idea of why you might need your app store platform and a detailed guide on how to build it.


Reasons for building your own app store

The first question that might arise is why would anyone need a store when there are already Google Play and AppStore. However, Artem shared two potential situations when such a solution can be very reasonable — and these cases include both personal and business purposes.


Reason 1: Building a corporate infrastructure

Many companies have their operations based on particular apps. Keeping them in the in-house store might significantly simplify access and control over the whole infrastructure. Besides, you can disable downloads of local apps from the other stores for all employers’ devices.


Reason 2: Helping senior family members

We mentioned personal reasons, and here is one. Seniors often face issues getting around the modern app stores and struggle to find and install the apps they need. Your own store will solve this problem: you can create a very simplified interface and control what your loved ones install on their devices.


Creating App Store Platform: Ready Guide

So you are determined to implement such a solution in your company or need to build a store for other reasons. Artem shared the best practices from his team’s experience and we are ready to bring out a step-by-step guide including all necessary tools.


Step 1. Planning

Like with every project, it’s essential to determine the final goal of the whole work. In our case, it is basically an app that will help to install other apps. The next question is how this app will work. Here are the tools that we recommend using according to our experience:

Interface

Flutter

Native Logic

Kotlin

APK files downloading

Company’s own API

API

The Go Language

Another important point is how we are going to add the apps to the store. The work of a store platform implies that we require apps in the form of APK files. Some of them have direct links, published by developers. However, most apps are only available via Google Play, which means that the project requires a Google Play parser.


To sum up, the whole ecosystem of the future app store platform will consist of three parts:

Step 2. App Development

Flutter allows the creation of an application store with minimum time and effort. Thus, we will mention the most complicated points that might cause difficulties.


To manage state in the app, our team used two packages: provider along with state_notifier. Such a solution turned out to be the simplest and the most convenient option for a Flutter app state management.


Another important take: before a user can install an app from the storage, they will need to download it first — and this step requires testing. To test how it works, the team added the APK file to a server to make the file available for download. Now you need to download it in the application:

void download(String url, {Function? success, Function? error}) async {
	var name = basename(url);
	var request = http.Request('GET', Uri.parse(url));
	var response = client.send(request);
	String dir = (await getApplicationDocumentsDirectory()).path;

	List<List<int>> chunks = [];
	int downloaded = 0;

	response.asStream().listen((http.StreamedResponse r) {
    	r.stream.listen((List<int> chunk) {
            	print('downloaded: ${downloaded * Megabyte}');

            	chunks.add(chunk);
            	downloaded += chunk.length;
        	}, onDone: () async {
  	          print('downloaded: ${downloaded * Megabyte}');

            	var path = '$dir/$name';

            	File file = new File(path);
            	final Uint8List bytes = Uint8List(downloaded);
            	int offset = 0;
            	for (List<int> chunk in chunks) {
                    bytes.setRange(offset, offset + chunk.length, chunk);
                	offset += chunk.length;
            	}

            	await file.writeAsBytes(bytes);

            	if (success != null) {
                	success(path);
            	}

            	return;
        	}, onError: (err) async {
            	if (error != null) {
                	error();
            	}
    	});
	});
}


The `getApplicationDocumentsDirectory()` function returns the path to the directory where you need to download the APK file. With the help of the `basename(url)` function we get the file name; to start background loading, we use the `response.asStream()`, and when the downloading is finished, we use the callback `success(path);`.


As a result, we have the file and can install it. Here we require a part of a native Kotlin code. To get this code, we need to install the__pigeon__ package and create particular contracts in the pideons/install/apk.dart file.

import 'package:pigeon/pigeon.dart';


class InstallRequest {
  String? file;
}

class InstallResponse {
  bool? status;
}

@HostApi()
abstract class InstallApk {
  @async
  InstallResponse install(InstallRequest request);
}


Then, we generate interfaces for a native code and the Dart code:

flutter pub run pigeon \
  --input pigeons/install/apk.dart \
  --dart_out lib/pigeons/install/apk.dart \
  --java_out ./android/app/src/main/java/ru/kovardin/getapp/pigeons/install/Apk.java \
  --java_package "ru.kovardin.getapp.pigeons.install"


Pigeon generates Java files, but we can use them in the Kotlin native code. Now it’s important to support `InstallApk` interfaces from the generated Java code:

package ru.kovardin.getapp.install

import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.core.content.FileProvider
import ru.kovardin.getapp.pigeons.install.Apk
import java.io.File


open class Apk(val context: Activity): Apk.InstallApk {
	override fun install(request: Apk.InstallRequest, result: Apk.Result<Apk.InstallResponse>?) {
    	Log.d("APK", "install apk ${request.file}")

    	try {
        	val file = File(request.file ?: "")
        	val intent = Intent(Intent.ACTION_VIEW)
        	val authority =  context.getApplicationContext().getPackageName().toString() + ".provider"
        	val type = "application/vnd.android.package-archive"

        	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            	val apk: Uri = FileProvider.getUriForFile(
                    	context,
  	                  authority,
                    	file
            	)

                intent.setDataAndType(apk, type)
            	val infos: List<ResolveInfo> = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
            	for (info in infos) {
                    context.grantUriPermission(authority, apk, Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
            	}
                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
                intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)

        	} else {
            	intent.setAction(Intent.ACTION_VIEW)
                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
                intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
                intent.setDataAndType(Uri.fromFile(file), type)
        	}
            context.startActivity(intent)
    	} catch (e: Exception) {
        	e.printStackTrace()
    	}

    	val response = Apk.InstallResponse();
    	response.status = true;
    	result?.success(response)
	}
}

Overall, the app installation is the launch of intent with the right type and the APK file in parameters. The rest of the work is done by Android — automatically.


The next thing we recommend to do is to check the SDK version. The future logic we will use to work with the APK file will depend on this SDK version. For SDK 23 or earlier versions, we can use a standard access to a file: `Uri.fromFile(file)`. This code is executed in the else section:

intent.setAction(Intent.ACTION_VIEW)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
intent.setDataAndType(Uri.fromFile(file), type)


Things get more complicated when you work with SDK 24 or later versions. To do things correctly, we will need to add a separate section to the AndroidManifest.xml file:

<application
    	android:allowBackup="true"
        android:label="@string/app_name">
    	<provider
            android:name="android.support.v4.content.FileProvider"
        	android:authorities="${applicationId}.authorityStr"
            android:exported="false"
            android:grantUriPermissions="true">
        	<meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/paths"/>
    	</provider>
</application>


The`@xml/paths` is a separate resource file: paths.xml from the xml folder. We need to define this file’s content:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
	<external-path
            name="files_root"
            path="Android/data/${applicationId}" />
	<external-path
            name="external_files"
        	path="." />
	<root-path name="root" path="/data/" />
</paths>

You will find a detailed guide on working with files in the Android SDK 24+ in developer documentation.


Now we can call the native code from the Dart code. Pigeon generated the InstallApk class that will be used in the service.

final apk = InstallApk();
final response = await apk.install(InstallRequest(
	file: apk,
));


Another essential point is that you need to request a user permission before installing an APK. You can easily do it with the permission_handler package.

Permission.storage.request().then((value) {
	print(value);

    Permission.requestInstallPackages.request().then((value) {
    	print(value);
	});
});

Step 3: API

The easiest way to create API is to stick to the Go language, which perfectly suits these needs. To work with HTTP, our team recommends to use chi: a very simple, but efficient router.

func (a *Api) Route(r chi.Router) {
  	r.Route("/v1", func(r chi.Router) {

        	r.Route("/apps", func(r chi.Router) {
              	r.Route("/{bundle}", func(r chi.Router) {
                    	r.Get("/", a.apps.One)
                    	r.Get("/download", a.apps.Download)
              	})
              	r.Get("/search", a.apps.Search)
              	r.Post("/updates", a.apps.Updates)
        	})

    	r.Get("/file/*", a.static.File)

        	// ...

  	})
}


All APK files are stored in the AWS. To share them correctly, a service needs a handler, that will proxy all requests to files and add a special header: `headers.Set ("Content-Type", "application/vnd.android.package-archive"). This header is required to download files exactly in the .apk format: if you don’t use it, some Android devices will download them as .zip archives.

func (h Static) File(w http.ResponseWriter, r *http.Request) {
  	headers := http.Header{}

  	ext := path.Ext(r.URL.Path)
  	if ext == ".apk" {
        	headers.Set("Content-Type", "application/vnd.android.package-archive")
  	}

  	proxy := httputil.ReverseProxy{
        	Director:  func(r *http.Request) {},
        	Transport: responseHeadersTransport(headers),
  	}

  	target := h.config.Scheme + path.Join(h.config.Cloud, strings.Replace(r.URL.Path, "file/", "", 1))
  	req, _ := http.NewRequest("GET", target, nil)
  	proxy.ServeHTTP(w, req)
}


The rest of the work with API is standard, so we won’t call special attention to the further actions.


Part 4: Parser To create an application store, you obviously need applications. They are all available in a ready store, and the easiest way is to get them from there. We are speaking about the Google Play that we need to parse — for information purposes only, of course.

GitHub has a ready library for downloading APK files: googleplay.


To start downloading files, you need to log in to Google Play:

googleplay -email EMAIL -password PASSWORD


This command will generate a special token file that we will use for requests. Then we generate a file with a device identifier — all requests will be performed from it:

googleplay -device


After everything is done, you will get access to information about an app you want to download:

> googleplay -a com.google.android.youtube
Title: YouTube
Creator: Google LLC
UploadDate: 2022-05-12
VersionString: 17.19.34
VersionCode: 1529337280
NumDownloads: 11.822 B
Size: 46.727 MB
File: APK APK APK APK
Offer: 0 USD


And download a particular version of any application:

googleplay -a com.google.android.youtube -v 1529337280

The Result

The AppLab team got the following platform as a result of all these efforts:



It looks and works like a normal application store. And, after all, it turned out to be comparatively easy to develop. Artem insists that most part of the work lies on Android itself, while a developer needs to correctly download the APK files, get all permissions from a user, and launch a standard installation for Android.


The result you see in the pictures shows an application store with a limited set of functions. All that it allows you to do is install apps on Android devices — not bad already, though. However, you can go further and add more things: Push Notifications via firebase_messaging package, or installed_apps package to realize the updates logic.