Skip to content

Commit

Permalink
Redesign the app (un)install receiver
Browse files Browse the repository at this point in the history
This change is aimed at improving the mechanisms for refreshing the app list
as before they showed the following, numerous flaws:
 - Failing on Android Oreo
 - No automatic data refresh after a system language change
 - Icons are never refreshed
 - Always running a full, slow refresh
 - Manual refreshing for troubleshooting is cumbersome
  • Loading branch information
SebiderSushi committed Jul 1, 2019
1 parent e0d044c commit bcce4b8
Show file tree
Hide file tree
Showing 20 changed files with 817 additions and 322 deletions.
11 changes: 7 additions & 4 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,18 @@

<receiver android:name=".background.AppInstallOrRemoveReceiver">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_INSTALL" />
<action android:name="android.intent.action.PACKAGE_ADDED" />
<action android:name="android.intent.action.PACKAGE_REMOVED" />
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<action android:name="android.intent.action.PACKAGE_CHANGED" />
<action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
<action android:name="android.intent.action.PACKAGE_REMOVED" />
<data android:scheme="package" />
</intent-filter>
</receiver>

<receiver android:name=".background.LocaleChangeReceiver">
<intent-filter>
<action android:name="android.intent.action.LOCALE_CHANGED" />
</intent-filter>
</receiver>
</application>

</manifest>
9 changes: 9 additions & 0 deletions android/src/main/java/org/ligi/fast/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@

import android.app.Activity;
import android.app.Application;
import android.content.pm.ChangedPackages;
import android.os.Build;
import android.util.Log;

import org.ligi.fast.background.BackgroundGatherAsyncTask;
import org.ligi.fast.background.ChangedPackagesAsyncTask;
import org.ligi.fast.model.AppInfoList;
import org.ligi.fast.settings.AndroidFASTSettings;
import org.ligi.fast.settings.FASTSettings;
import org.ligi.tracedroid.TraceDroid;

import java.io.File;
import java.lang.ref.WeakReference;

public class App extends Application {

Expand All @@ -22,13 +28,16 @@ public interface PackageChangedListener {
}

public static PackageChangedListener packageChangedListener;
public static WeakReference<AppInfoList> backingAppInfoList;

@Override
public void onCreate() {
super.onCreate();
appInstance = this;
TraceDroid.init(this);
settings = new AndroidFASTSettings(App.this);

Log.d(LOG_TAG, "onCreate");
}

public static FASTSettings getSettings() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,216 @@
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.util.Log;

import org.ligi.fast.App;
import org.ligi.fast.model.AppInfo;
import org.ligi.fast.model.AppInfoList;
import org.ligi.fast.util.AppInfoListStore;

import java.util.Iterator;
import java.util.List;

/**
* Whenever an app is installed, uninstalled or components change
* (e.g. the app disabled one of it's activities to hide it from the launcher)
* this receiver takes care of removing or updating corresponding entries
* (namely all activities and aliases) from AppInfoList and deletes their icons
* from cache to clean up when uninstalling or to cause a refresh when updating.
*/
public class AppInstallOrRemoveReceiver extends BroadcastReceiver {

@Override
public void onReceive(Context context, Intent intent) {
final AppInfoListStore appInfoListStore = new AppInfoListStore(context);
if (Build.VERSION.SDK_INT < 11) {
new AppInstallOrRemoveAsyncTask(context.getApplicationContext(), intent).doInBackground(new Void[1]);
} else {
final PendingResult pendingResult = goAsync();
new AppInstallOrRemoveAsyncTask(context.getApplicationContext(), intent, pendingResult).execute();
}
}

private static class AppInstallOrRemoveAsyncTask extends AsyncTask<Void, AppInfo, Void> {
private BroadcastReceiver.PendingResult mPendingResult;
private Context mContext;
private Intent mIntent;
private AppInfoListStore mAppInfoListStore;

private void save(AppInfoList appInfoList) {
if (App.packageChangedListener == null) {
if (App.backingAppInfoList != null) {
AppInfoList backingList = App.backingAppInfoList.get();
backingList.clear();
backingList.addAll(appInfoList);
} else {
mAppInfoListStore.save(appInfoList);
}
} else {
App.packageChangedListener.onPackageChange(appInfoList);
}
}

AppInstallOrRemoveAsyncTask(Context context, Intent intent) {
this.mContext = context;
this.mIntent = intent;
}

AppInstallOrRemoveAsyncTask(Context context, Intent intent, BroadcastReceiver.PendingResult pendingResult) {
this(context, intent);
this.mPendingResult = pendingResult;
}

@Override
protected Void doInBackground(Void... params) {
long start = System.currentTimeMillis();
Uri data = mIntent.getData();
if (data == null) return null; // This should never be the case
String packageName = data.getSchemeSpecificPart();
String action = mIntent.getAction();

Log.d(App.LOG_TAG, "BroadcastReceiver: Action - " + action + "; Package - " + packageName);

if (App.packageChangedListener == null) {
App.packageChangedListener = new App.PackageChangedListener() {
@Override
public void onPackageChange(AppInfoList appInfoList) {
appInfoListStore.save(appInfoList);
//TODO: When moving to MinApiLevel 14 or higher, refactor Intent.ACTION_PACKAGE_REMOVED
// to Intent.ACTION_PACKAGE_FULLY_REMOVED and remove the following block

// If this is the removal Broadcast during an upgrade don't to do anything.
// Data will be updated in the next invocation by the package added Broadcast.
// This prevents running the update twice.
// getBooleanExtra defaultValue is false so that this is not also tripped
// in case of a full uninstall where this extra is missing.
// NOTE: This is only due to compatibility below API level 14
// When using the Intent.ACTION_PACKAGE_FULLY_REMOVED instead of
// Intent.ACTION_PACKAGE_REMOVED this is not necessary
boolean replacingDefaultFalse = mIntent.getBooleanExtra(Intent.EXTRA_REPLACING, false);
if (Intent.ACTION_PACKAGE_REMOVED.equals(action) && replacingDefaultFalse) return null;

mAppInfoListStore = new AppInfoListStore(mContext);
AppInfoList appInfoList;
if (App.backingAppInfoList != null) {
appInfoList = new AppInfoList();
appInfoList.addAll(App.backingAppInfoList.get());
if (appInfoList.size() == 0) {
appInfoList = mAppInfoListStore.load();
}
} else {
appInfoList = mAppInfoListStore.load();
}

// Check the package is newly installed or not
// getBooleanExtra defaultValue is true so that in case of doubt the
// presence of old information is checked anyway
boolean replacingDefaultTrue = mIntent.getBooleanExtra(Intent.EXTRA_REPLACING, true);
boolean newInstall = Intent.ACTION_PACKAGE_ADDED.equals(action) && !replacingDefaultTrue;
AppInfoList matchedAppInfoList = new AppInfoList();
// If this is not a new install, i.e. update or uninstall, then collect the existing records
// already held about this app into matchedAppInfoList and remove them from the main list.
// That way if uninstalling an app the main list is already updated after this step.
// If it is an update only matchedAppInfoList will have to be iterated in the next step
// since it contains all records that need an update.
// After this all old info & icons of the affected package should be cleaned up.
if (!newInstall) {
for (Iterator<AppInfo> iterator = appInfoList.iterator(); iterator.hasNext(); ) {
AppInfo appInfo = iterator.next();
if (appInfo.getPackageName().equals(packageName)) {
matchedAppInfoList.add(appInfo);
iterator.remove();
// Delete the current icon to force a refresh
//File icon = appInfo.getIconCacheFile();
//icon.delete();
}
}
// Just to be sure; If this is the case there is no old information
// to update and new information can simply be added in the next step
newInstall = matchedAppInfoList.size() == 0;
}

// If not uninstalled go on to update/add new information
// Otherwise everything is done and the list just has to be saved
if (!Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
Intent launcherIntent = new Intent(Intent.ACTION_MAIN);
launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER);
launcherIntent.setPackage(packageName);
List<ResolveInfo> resolveInfoList = mContext.getPackageManager().queryIntentActivities(launcherIntent, 0);

Intent homeIntent = new Intent(Intent.ACTION_MAIN);
homeIntent.addCategory(Intent.CATEGORY_HOME);
homeIntent.setPackage(packageName);
List<ResolveInfo> homeInfoList = mContext.getPackageManager().queryIntentActivities(homeIntent, 0);

// If there are no activities that should be displayed on the launcher we can quit here
if (resolveInfoList.size() == 0 && homeInfoList.size() == 0) {
save(appInfoList);
long end = System.currentTimeMillis();
long duration = end - start;
Log.d(App.LOG_TAG, "BroadcastReceiver ran short " + duration + "ms.");
return null;
}
};

// Deduplicate Resolve Info of activities with both categories - like SearchActivity (see manifest)
for (ResolveInfo info : resolveInfoList) {
Iterator<ResolveInfo> homeIterator = homeInfoList.iterator();
while (homeIterator.hasNext()) {
ResolveInfo homeInfo = homeIterator.next();
if (homeInfo.activityInfo.name.equals(info.activityInfo.name)) {
homeIterator.remove();
break;
}
}
if (!homeIterator.hasNext()) {
break;
}
}
resolveInfoList.addAll(homeInfoList);

if (newInstall) { // New app, simple adding
for (ResolveInfo info : resolveInfoList) {
appInfoList.add(new AppInfo(mContext, info));
}
} else { // Update, merge data
for (ResolveInfo info : resolveInfoList) {
AppInfo newAppInfo = new AppInfo(mContext, info);

Iterator<AppInfo> oldInfoIterator = matchedAppInfoList.iterator();
while (oldInfoIterator.hasNext()) {
AppInfo oldInfo = oldInfoIterator.next();
if (oldInfo.getActivityName().equals(newAppInfo.getActivityName())) {
if (oldInfo.getLabelMode() == 2) { // AppInfo is alias
oldInfo.setLabel(newAppInfo.getLabel());
oldInfo.setInstallTime(newAppInfo.getInstallTime());
appInfoList.add(oldInfo);
} else {
newAppInfo.setCallCount(oldInfo.getCallCount());
newAppInfo.setPinMode(oldInfo.getPinMode());
newAppInfo.setLabelMode(oldInfo.getLabelMode());
newAppInfo.setOverrideLabel(oldInfo.getOverrideLabel());
}
oldInfoIterator.remove();
}
}
appInfoList.add(newAppInfo);
}
}
}

save(appInfoList);
mContext = null;
long end = System.currentTimeMillis();
long duration = end - start;
Log.d(App.LOG_TAG, "BroadcastReceiver ran " + duration + "ms.");
return null;
}

new BackgroundGatherAsyncTask(context, appInfoListStore.load()).execute();
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
if (Build.VERSION.SDK_INT >= 11) {
// Must call finish() so the BroadcastReceiver can be recycled.
mPendingResult.finish();
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@

import org.ligi.fast.App;
import org.ligi.fast.model.AppInfo;
import org.ligi.fast.model.AppInfoList;
import org.ligi.fast.util.AppInfoListStore;

import java.util.List;

public class BackgroundGatherAsyncTask extends BaseAppGatherAsyncTask {

public BackgroundGatherAsyncTask(Context context, AppInfoList oldAppInfoList) {
super(context, oldAppInfoList);
private Context context;

public BackgroundGatherAsyncTask(Context context) {
super(context);
this.context = context;
}

@Override
Expand All @@ -25,7 +25,9 @@ protected void onPostExecute(Void result) {
super.onPostExecute(result);
if (App.packageChangedListener != null) {
App.packageChangedListener.onPackageChange(appInfoList);
} else {
new AppInfoListStore(context).save(appInfoList);
}
context = null;
}

}
Loading

0 comments on commit bcce4b8

Please sign in to comment.