Commit 1cdfba6c authored by Nihar Thakkar's avatar Nihar Thakkar
Browse files

Implement updates screen

parent a17387cf
......@@ -27,6 +27,8 @@ android {
dependencies {
def lifecycle_version = "1.1.1"
def work_version = "1.0.0-beta01"
implementation "android.arch.work:work-runtime:$work_version"
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:27.1.1'
......
......@@ -10,6 +10,7 @@ import android.support.design.widget.Snackbar
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.view.MenuItem
import androidx.work.*
import io.eelo.appinstaller.applicationmanager.ApplicationManager
import io.eelo.appinstaller.applicationmanager.ApplicationManagerServiceConnection
import io.eelo.appinstaller.applicationmanager.ApplicationManagerServiceConnectionCallback
......@@ -18,9 +19,11 @@ import io.eelo.appinstaller.home.HomeFragment
import io.eelo.appinstaller.search.SearchFragment
import io.eelo.appinstaller.settings.SettingsFragment
import io.eelo.appinstaller.updates.UpdatesFragment
import io.eelo.appinstaller.updates.model.UpdatesWorker
import io.eelo.appinstaller.utils.Constants
import io.eelo.appinstaller.utils.Constants.CURRENTLY_SELECTED_FRAGMENT_KEY
import kotlinx.android.synthetic.main.activity_main.*
import java.util.concurrent.TimeUnit
class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener,
ApplicationManagerServiceConnectionCallback {
......@@ -38,6 +41,8 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
bottom_navigation_view.setOnNavigationItemSelectedListener(this)
disableShiftingOfNabBarItems()
initialiseUpdatesWorker()
// Show the home fragment by default
currentFragmentId = if (savedInstanceState != null &&
savedInstanceState.containsKey(CURRENTLY_SELECTED_FRAGMENT_KEY)) {
......@@ -49,6 +54,19 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
applicationManagerServiceConnection.bindService(this)
}
private fun initialiseUpdatesWorker() {
val constraints = Constraints.Builder().apply {
setRequiresBatteryNotLow(true)
setRequiredNetworkType(NetworkType.CONNECTED)
}.build()
val updatesCheckBuilder = PeriodicWorkRequest
.Builder(UpdatesWorker::class.java, 15, TimeUnit.MINUTES).apply {
setConstraints(constraints)
}
WorkManager.getInstance().enqueueUniquePeriodicWork(Constants.UPDATES_WORK_NAME,
ExistingPeriodicWorkPolicy.KEEP, updatesCheckBuilder.build())
}
override fun onServiceBind(applicationManager: ApplicationManager) {
initialiseFragments(applicationManager)
selectFragment(currentFragmentId)
......
......@@ -117,6 +117,13 @@ class Application(val packageName: String, private val applicationManager: Appli
return uses.get() != 0
}
fun assertBasicData(context: Context): Error? {
if (basicData != null) {
return null
}
return findBasicData(context)
}
fun assertFullData(context: Context): Error? {
if (fullData != null) {
return null
......
......@@ -13,6 +13,7 @@ import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import io.eelo.appinstaller.R
import io.eelo.appinstaller.application.model.Application
import io.eelo.appinstaller.applicationmanager.ApplicationManager
import io.eelo.appinstaller.common.ApplicationListAdapter
import io.eelo.appinstaller.updates.viewModel.UpdatesViewModel
......@@ -22,6 +23,7 @@ class UpdatesFragment : Fragment() {
private lateinit var updatesViewModel: UpdatesViewModel
private var applicationManager: ApplicationManager? = null
private lateinit var recyclerView: RecyclerView
private var applicationList = ArrayList<Application>()
fun initialise(applicationManager: ApplicationManager) {
this.applicationManager = applicationManager
......@@ -36,25 +38,42 @@ class UpdatesFragment : Fragment() {
updatesViewModel = ViewModelProviders.of(activity!!).get(UpdatesViewModel::class.java)
recyclerView = view.findViewById(R.id.app_list)
val splashContainer = view.findViewById<LinearLayout>(R.id.splash_container)
val progressBar = view.findViewById<ProgressBar>(R.id.progress_bar)
val errorContainer = view.findViewById<LinearLayout>(R.id.error_container)
val errorDescription = view.findViewById<TextView>(R.id.error_description)
// Initialise UI elements
updatesViewModel.initialise(applicationManager!!)
initializeRecyclerView()
recyclerView.visibility = View.GONE
progressBar.visibility = View.VISIBLE
errorContainer.visibility = View.GONE
splashContainer.visibility = View.GONE
view.findViewById<TextView>(R.id.error_resolve).setOnClickListener {
progressBar.visibility = View.VISIBLE
updatesViewModel.loadApplicationList(context!!)
}
// Bind recycler view adapter to search results list in view model
// Initialise recycler view
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.adapter = ApplicationListAdapter(activity!!, applicationList)
// Bind recycler view adapter to outdated applications list in view model
updatesViewModel.getApplications().observe(this, Observer {
if (it!!.isNotEmpty()) {
recyclerView.adapter.notifyDataSetChanged()
if (it != null) {
applicationList.clear()
applicationList.addAll(it)
progressBar.visibility = View.GONE
recyclerView.adapter.notifyDataSetChanged()
recyclerView.scrollToPosition(0)
if (applicationList.isEmpty()) {
recyclerView.visibility = View.GONE
splashContainer.visibility = View.VISIBLE
} else {
splashContainer.visibility = View.GONE
recyclerView.visibility = View.VISIBLE
}
}
})
......@@ -64,6 +83,8 @@ class UpdatesFragment : Fragment() {
errorDescription.text = activity!!.getString(Common.getScreenErrorDescriptionId(it))
errorContainer.visibility = View.VISIBLE
progressBar.visibility = View.GONE
splashContainer.visibility = View.GONE
recyclerView.visibility = View.GONE
} else {
errorContainer.visibility = View.GONE
}
......@@ -74,25 +95,23 @@ class UpdatesFragment : Fragment() {
return view
}
private fun initializeRecyclerView() {
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.adapter = ApplicationListAdapter(activity!!, updatesViewModel.getApplications().value!!)
}
override fun onResume() {
super.onResume()
if (::updatesViewModel.isInitialized) {
updatesViewModel.getApplications().value!!.forEach { application ->
application.checkForStateUpdate(context!!)
updatesViewModel.getApplications().value?.let {
it.forEach { application ->
application.checkForStateUpdate(context!!)
}
}
}
}
fun decrementApplicationUses() {
if (::updatesViewModel.isInitialized) {
updatesViewModel.getApplications().value!!.forEach {
it.decrementUses()
updatesViewModel.getApplications().value?.let {
it.forEach { application ->
application.decrementUses()
}
}
}
}
......
package io.eelo.appinstaller.updates.model
import android.content.Context
import android.os.AsyncTask
import io.eelo.appinstaller.application.model.Application
import io.eelo.appinstaller.application.model.State
import io.eelo.appinstaller.applicationmanager.ApplicationManager
import io.eelo.appinstaller.utils.Constants
import java.io.BufferedReader
import java.io.InputStreamReader
import java.lang.Exception
class OutdatedApplicationsFileReader(private val applicationManager: ApplicationManager,
private val callback: UpdatesModelInterface) :
AsyncTask<Context, Void, ArrayList<Application>>() {
override fun doInBackground(vararg context: Context): ArrayList<Application> {
val applications = ArrayList<Application>()
try {
context[0].openFileInput(Constants.OUTDATED_APPLICATIONS_FILENAME).use {
val inputStreamReader = InputStreamReader(it)
val bufferedReader = BufferedReader(inputStreamReader)
bufferedReader.forEachLine { packageName ->
val application = applicationManager.findOrCreateApp(packageName)
val error = application.assertBasicData(context[0])
if (error == null) {
if (application.state == State.NOT_UPDATED) {
applications.add(application)
}
}
}
bufferedReader.close()
inputStreamReader.close()
it.close()
}
} catch (exception: Exception) {
exception.printStackTrace()
}
return applications
}
override fun onPostExecute(result: ArrayList<Application>) {
callback.onAppsFound(result)
}
}
......@@ -6,10 +6,11 @@ import android.os.AsyncTask
import io.eelo.appinstaller.application.model.Application
import io.eelo.appinstaller.applicationmanager.ApplicationManager
import io.eelo.appinstaller.application.model.State
import io.eelo.appinstaller.utils.Execute
import java.util.concurrent.atomic.AtomicInteger
class OutdatedApplicationsFinder(private val packageManager: PackageManager, private val callback: UpdatesModelInterface, private val applicationManager: ApplicationManager) : AsyncTask<Context, Any, Any>() {
class OutdatedApplicationsFinder(private val packageManager: PackageManager,
private val callback: UpdatesWorkerInterface,
private val applicationManager: ApplicationManager) :
AsyncTask<Context, Any, Any>() {
private var result: ArrayList<Application>? = null
......@@ -19,40 +20,27 @@ class OutdatedApplicationsFinder(private val packageManager: PackageManager, pri
}
override fun onPostExecute(result: Any?) {
callback.onAppsFound(this.result!!)
callback.onApplicationsFound(this.result!!)
}
private fun getOutdatedApplications(context: Context): ArrayList<Application> {
val result = ArrayList<Application>()
val installedApplications = getInstalledApplications()
val waitingTasks = AtomicInteger(installedApplications.size)
val blocker = Object()
synchronized(blocker) {
installedApplications.forEach { packageName ->
val application = applicationManager.findOrCreateApp(packageName)
Execute({
verifyApplication(application, waitingTasks, blocker, result, context)
}, {})
}
blocker.wait()
installedApplications.forEach { packageName ->
val application = applicationManager.findOrCreateApp(packageName)
verifyApplication(application, result, context)
}
return result
}
private fun verifyApplication(application: Application, waitingTasks: AtomicInteger, blocker: Object, apps: ArrayList<Application>, context: Context) {
private fun verifyApplication(application: Application, apps: ArrayList<Application>,
context: Context) {
val error = application.assertFullData(context)
if (error == null && application.state == State.NOT_UPDATED) {
apps.add(application)
} else {
application.decrementUses()
}
if (waitingTasks.decrementAndGet() == 0) {
synchronized(blocker) {
blocker.notify()
}
}
}
private fun getInstalledApplications(): ArrayList<String> {
......
......@@ -2,6 +2,7 @@ package io.eelo.appinstaller.updates.model
import android.arch.lifecycle.MutableLiveData
import android.content.Context
import android.os.AsyncTask
import io.eelo.appinstaller.application.model.Application
import io.eelo.appinstaller.applicationmanager.ApplicationManager
import io.eelo.appinstaller.utils.Common
......@@ -11,17 +12,12 @@ class UpdatesModel : UpdatesModelInterface {
val applicationList = MutableLiveData<ArrayList<Application>>()
var screenError = MutableLiveData<Error>()
init {
if (applicationList.value == null) {
applicationList.value = ArrayList()
}
}
var applicationManager: ApplicationManager? = null
override fun loadApplicationList(context: Context) {
if (Common.isNetworkAvailable(context)) {
OutdatedApplicationsFinder(context.packageManager, this, applicationManager!!).execute(context)
OutdatedApplicationsFileReader(applicationManager!!, this)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, context)
} else {
screenError.value = Error.NO_INTERNET
}
......
package io.eelo.appinstaller.updates.model
import android.content.Context
import android.os.AsyncTask
import androidx.work.Worker
import androidx.work.WorkerParameters
import io.eelo.appinstaller.application.model.Application
import io.eelo.appinstaller.applicationmanager.ApplicationManager
import io.eelo.appinstaller.utils.Constants
class UpdatesWorker(context: Context, params: WorkerParameters) : Worker(context, params),
UpdatesWorkerInterface {
private val blocker = Object()
override fun doWork(): Result {
val applicationManager = ApplicationManager()
applicationManager.start(applicationContext)
loadOutdatedApplications(applicationManager)
return Result.success()
}
private fun loadOutdatedApplications(applicationManager: ApplicationManager) {
OutdatedApplicationsFinder(applicationContext.packageManager, this,
applicationManager).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
applicationContext)
synchronized(blocker) {
blocker.wait()
}
}
override fun onApplicationsFound(applications: ArrayList<Application>) {
applicationContext.openFileOutput(Constants.OUTDATED_APPLICATIONS_FILENAME,
Context.MODE_PRIVATE).use {
applications.forEach { application ->
it.write((application.basicData!!.packageName + "\n").toByteArray())
}
it.close()
}
synchronized(blocker) {
blocker.notify()
}
}
}
package io.eelo.appinstaller.updates.model
import io.eelo.appinstaller.application.model.Application
interface UpdatesWorkerInterface {
fun onApplicationsFound(applications: ArrayList<Application>)
}
......@@ -14,6 +14,10 @@ class UpdatesViewModel : ViewModel(), UpdatesViewModelInterface {
override fun initialise(applicationManager: ApplicationManager) {
updatesModel.applicationManager = applicationManager
if (updatesModel.applicationList.value != null &&
updatesModel.applicationList.value!!.isEmpty()) {
updatesModel.applicationList.value = null
}
}
override fun getApplications(): MutableLiveData<ArrayList<Application>> {
......
......@@ -20,12 +20,14 @@ object Constants {
const val APPLICATION_PACKAGE_NAME_KEY = "application_package_name"
const val APPLICATION_DESCRIPTION_KEY = "application_description"
const val SELECTED_APPLICATION_SCREENSHOT_KEY = "selected_application_screenshot"
const val DOWNLOAD_NOTIFICATION_ID = 102
const val DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_notification_channel"
// Categories
const val CATEGORY_KEY = "category_key"
// Home
const val CURRENTLY_SELECTED_FRAGMENT_KEY = "currently_selected_fragment"
// Updates
const val OUTDATED_APPLICATIONS_FILENAME = "outdated_applications.txt"
const val UPDATES_WORK_NAME = "updates_work"
}
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="512dp"
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:pathData="M435.848,83.466L172.804,346.51l-96.652,-96.652c-4.686,-4.686 -12.284,-4.686 -16.971,0l-28.284,28.284c-4.686,4.686 -4.686,12.284 0,16.971l133.421,133.421c4.686,4.686 12.284,4.686 16.971,0l299.813,-299.813c4.686,-4.686 4.686,-12.284 0,-16.971l-28.284,-28.284c-4.686,-4.686 -12.284,-4.686 -16.97,0z"/>
</vector>
......@@ -12,6 +12,15 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include
layout="@layout/updates_splash_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include
layout="@layout/error_layout"
android:layout_width="wrap_content"
......
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/splash_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
android:padding="@dimen/layout_padding_large">
<ImageView
android:layout_width="64dp"
android:layout_height="64dp"
android:src="@drawable/ic_all_apps_updated"
android:tint="@android:color/darker_gray" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_margin_large"
android:maxLines="2"
android:text="@string/updates_splash_title"
android:textAlignment="center"
android:textColor="@android:color/darker_gray"
android:textSize="@dimen/text_size_large" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="update_interval_names">
<item>Hourly</item>
<item>Daily</item>
<item>Weekly</item>
<item>Monthly</item>
</string-array>
<string-array name="update_interval_values">
<item>1</item>
<item>24</item>
<item>168</item>
<item>720</item>
......
......@@ -60,6 +60,9 @@
<!-- Search Fragment -->
<string name="search_description">Search for an app</string>
<!-- Updates -->
<string name="updates_splash_title">All apps are up-to-date</string>
<!-- Settings Fragment -->
<string name="preference_theme_title">Theme</string>
<string name="preference_theme_dark_title">Dark theme</string>
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment