4

Background

In the past, I've found a special app called "Purchased apps" that somehow gets a list of the apps you've purchased. Not seeing any API for this, I asked how does it do it (and sadly still couldn't find a clear answer and a POC to demonstrate it).

The problem

Time passed, and I've noticed there is actually an open sourced app called "Aurora Store" (repository here) that can get about as much information as the Play Store. Screenshot from it:

enter image description here

Thing is, I got issues trying to figure out how to use its code properly, and the weird thing is that those apps get the information from different sources.

What I've tried

So, seeing it allows you to login to Google, and then get the "library" information (history of installed apps), I decided to give it a go (full sample on Github, here) :

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private lateinit var webView: WebView
    private val cookieManager = CookieManager.getInstance()

    @SuppressLint("SetJavaScriptEnabled")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
        val cachedEmail = defaultSharedPreferences.getString("email", null)
        val cachedAasToken = defaultSharedPreferences.getString("aasToken", null)
        if (cachedEmail != null && cachedAasToken != null) {
            onGotAasToken(applicationContext, cachedEmail, cachedAasToken)
        } else {
            webView = findViewById(R.id.webView)
            cookieManager.removeAllCookies(null)
            cookieManager.acceptThirdPartyCookies(webView)
            cookieManager.setAcceptThirdPartyCookies(webView, true)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                webView.settings.safeBrowsingEnabled = false
            }
            webView.webViewClient = object : WebViewClient() {
                override fun onPageFinished(view: WebView, url: String) {
                    val cookies = CookieManager.getInstance().getCookie(url)
                    val cookieMap: MutableMap<String, String> = AC2DMUtil.parseCookieString(cookies)
                    val oauthToken: String? = cookieMap[AUTH_TOKEN]
                    oauthToken?.let {
                        webView.evaluateJavascript("(function() { return document.getElementById('profileIdentifier').innerHTML; })();") {
                            val email = it.replace("\"".toRegex(), "")
                            Log.d("AppLog", "got email?${email.isNotBlank()} got oauthToken?${oauthToken.isNotBlank()}")
                            buildAuthData(applicationContext, email, oauthToken)
                        }
                    } ?: Log.d("AppLog", "could not get oauthToken")
                }
            }
            webView.settings.apply {
                allowContentAccess = true
                databaseEnabled = true
                domStorageEnabled = true
                javaScriptEnabled = true
                cacheMode = WebSettings.LOAD_DEFAULT
            }
            webView.loadUrl(EMBEDDED_SETUP_URL)
        }
    }

    companion object {
        const val EMBEDDED_SETUP_URL =
                "https://accounts.google.com/EmbeddedSetup/identifier?flowName=EmbeddedSetupAndroid"
        const val AUTH_TOKEN = "oauth_token"

        private fun buildAuthData(context: Context, email: String, oauthToken: String?) {
            thread {
                try {
                    val aC2DMResponse: Map<String, String> =
                            AC2DMTask().getAC2DMResponse(email, oauthToken)
                    val aasToken = aC2DMResponse["Token"]!!
                    PreferenceManager.getDefaultSharedPreferences(context)
                            .edit().putString("email", email).putString("aasToken", aasToken).apply()
                    onGotAasToken(context, email, aasToken)
                } catch (e: Exception) {
                    e.printStackTrace()
                }
            }
        }

        private fun onGotAasToken(context: Context, email: String, aasToken: String) {
            thread {
                val properties = NativeDeviceInfoProvider(context).getNativeDeviceProperties()
                val authData = AuthHelper.build(email, aasToken, properties)
                val purchaseHelper = PurchaseHelper(authData).using(HttpClient.getPreferredClient())
                var offset = 0
                Log.d("AppLog", "list of purchase history:")
                while (true) {
                    val purchaseHistory = purchaseHelper.getPurchaseHistory(offset)
                    if (purchaseHistory.isNullOrEmpty())
                        break
                    val size = purchaseHistory.size
                    offset += size
                    purchaseHistory.forEach {
                        Log.d("AppLog", "${it.packageName} ${it.displayName}")
                    }
                }
                Log.d("AppLog", "done")
            }
        }
    }
}

It seems it got the token it needs (and the email), but sadly it seems to get 2 apps and that's it, and then when I try to get the next ones, I get the same 2 apps, twice more, meaning as such:

list of purchase history:
dev.southpaw.dungeon Dungeon Live Wallpaper
com.crydata.mylivewallpaper Hex AMOLED Neon Live Wallpaper 2021
dev.southpaw.dungeon Dungeon Live Wallpaper
com.crydata.mylivewallpaper Hex AMOLED Neon Live Wallpaper 2021
dev.southpaw.dungeon Dungeon Live Wallpaper
com.crydata.mylivewallpaper Hex AMOLED Neon Live Wallpaper 2021

and on the last time it tries to get the next chunk of apps, it crashes with this exception:

FATAL EXCEPTION: Thread-4
    Process: com.lb.getplaystoreinstalledappshistory, PID: 6149
    Server(code=400, reason=Bad Request)
        at com.aurora.gplayapi.helpers.AppDetailsHelper.getAppByPackageName(AppDetailsHelper.kt:115)
        at com.aurora.gplayapi.helpers.PurchaseHelper.getPurchaseHistory(PurchaseHelper.kt:63)
        at com.lb.getplaystoreinstalledappshistory.MainActivity$Companion$onGotAasToken$1.invoke(MainActivity.kt:96)
        at com.lb.getplaystoreinstalledappshistory.MainActivity$Companion$onGotAasToken$1.invoke(MainActivity.kt:68)
        at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)

The questions

  1. What's wrong with how I tried to get the list of apps? How can I get it right, ordered by time installed?
  2. Is there any way to get the time they were installed (or any clue about it) ? Somehow the "Purchased apps" app got the time. Granted it was only for purchased apps, but still...
  3. The "Purchased apps" app even got login better, as it doesn't require user-name and password. Instead it offers a dialog to choose the account. Assuming I get it right, is it possible to get the same information using the same login dialog ?
apaderno
  • 28,547
  • 16
  • 75
  • 90
android developer
  • 114,585
  • 152
  • 739
  • 1,270

1 Answers1

0

Not sure if this information is remotely useful but might as well mention it...

I accidentally decompiled their archived APK from 2015 when they didn't minimize their code and at least back then, they were using a JSoup HTML parser spider. Possibly they still are, which is probably not allowed by Google and incredibly prone to maintenance.

enter image description here

Edward van Raak
  • 4,841
  • 3
  • 24
  • 38
  • This is the parsing itself (which is also important), but how do you get the login phase, and then the "html" parameter here ? Have you tried a POC for this? – android developer Apr 04 '21 at 21:31
  • Where can I get this not minimized APK that you decompiled? I'd like to have a look at it and maybe learn a few things from it. – user14678216 Apr 13 '21 at 07:15
  • @user14678216 Maybe from here: https://apkpure.com/purchased-apps-reinstall-your-paid-apps-games/com.azefsw.purchasedapps – android developer Apr 14 '21 at 22:24