package fr.neamar.kiss.dataprovider; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.LauncherApps; import android.os.Build; import android.os.Process; import android.os.UserManager; import android.util.Pair; import java.util.ArrayList; import fr.neamar.kiss.KissApplication; import fr.neamar.kiss.loader.LoadAppPojos; import fr.neamar.kiss.normalizer.StringNormalizer; import fr.neamar.kiss.pojo.AppPojo; import fr.neamar.kiss.pojo.Pojo; import fr.neamar.kiss.broadcast.PackageAddedRemovedHandler; import fr.neamar.kiss.utils.UserHandle; public class AppProvider extends Provider<AppPojo> { @Override public void onCreate() { if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // Package installation/uninstallation events for the main // profile are still handled using PackageAddedRemovedHandler itself final UserManager manager = (UserManager) this.getSystemService(Context.USER_SERVICE); final LauncherApps launcher = (LauncherApps) this.getSystemService(Context.LAUNCHER_APPS_SERVICE); launcher.registerCallback(new LauncherApps.Callback() { @Override public void onPackageAdded(String packageName, android.os.UserHandle user) { if(!Process.myUserHandle().equals(user)) { PackageAddedRemovedHandler.handleEvent(AppProvider.this, "android.intent.action.PACKAGE_ADDED", packageName, new UserHandle(manager.getSerialNumberForUser(user), user), false ); } } @Override public void onPackageChanged(String packageName, android.os.UserHandle user) { if(!Process.myUserHandle().equals(user)) { PackageAddedRemovedHandler.handleEvent(AppProvider.this, "android.intent.action.PACKAGE_ADDED", packageName, new UserHandle(manager.getSerialNumberForUser(user), user), true ); } } @Override public void onPackageRemoved(String packageName, android.os.UserHandle user) { if(!Process.myUserHandle().equals(user)) { PackageAddedRemovedHandler.handleEvent(AppProvider.this, "android.intent.action.PACKAGE_REMOVED", packageName, new UserHandle(manager.getSerialNumberForUser(user), user), false ); } } @Override public void onPackagesAvailable(String[] packageNames, android.os.UserHandle user, boolean replacing) { if(!Process.myUserHandle().equals(user)) { PackageAddedRemovedHandler.handleEvent(AppProvider.this, "android.intent.action.MEDIA_MOUNTED", null, new UserHandle(manager.getSerialNumberForUser(user), user), false ); } } @Override public void onPackagesUnavailable(String[] packageNames, android.os.UserHandle user, boolean replacing) { if(!Process.myUserHandle().equals(user)) { PackageAddedRemovedHandler.handleEvent(AppProvider.this, "android.intent.action.MEDIA_UNMOUNTED", null, new UserHandle(manager.getSerialNumberForUser(user), user), false ); } } }); // Try to clean up app-related data when profile is removed IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED); filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED); this.registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if(intent.getAction().equals(Intent.ACTION_MANAGED_PROFILE_ADDED)) { AppProvider.this.reload(); } else if(intent.getAction().equals(Intent.ACTION_MANAGED_PROFILE_REMOVED)) { android.os.UserHandle profile = (android.os.UserHandle) intent.getParcelableExtra(Intent.EXTRA_USER); UserHandle user = new UserHandle(manager.getSerialNumberForUser(profile), profile); KissApplication.getDataHandler(context).removeFromExcluded(user); KissApplication.getDataHandler(context).removeFromFavorites(user); AppProvider.this.reload(); } } }, filter); } super.onCreate(); } @Override public void reload() { this.initialize(new LoadAppPojos(this)); } public ArrayList<Pojo> getResults(String query) { query = StringNormalizer.normalize(query); ArrayList<Pojo> records = new ArrayList<>(); int relevance; int queryPos; // The position inside the query int normalizedAppPos; // The position inside pojo.nameNormalized int appPos; // The position inside pojo.name, updated after we increment normalizedAppPos int beginMatch ; int matchedWordStarts; int totalWordStarts; ArrayList<Pair<Integer, Integer>> matchPositions; for (AppPojo pojo : pojos) { pojo.displayName = pojo.name; pojo.displayTags = pojo.tags; relevance = 0; queryPos = 0; normalizedAppPos = 0; appPos = pojo.mapPosition(normalizedAppPos); beginMatch = 0; matchedWordStarts = 0; totalWordStarts = 0; matchPositions = null; boolean match = false; int inputLength = pojo.nameNormalized.length(); while (normalizedAppPos < inputLength) { int cApp = pojo.nameNormalized.codePointAt(normalizedAppPos); if (queryPos < query.length() && query.codePointAt(queryPos) == cApp) { // If we aren't already matching something, let's save the beginning of the match if (!match) { beginMatch = normalizedAppPos; match = true; } // If we are at the beginning of a word, add it to matchedWordStarts if (appPos == 0 || normalizedAppPos == 0 || Character.isUpperCase(pojo.name.codePointAt(appPos)) || Character.isWhitespace(pojo.name.codePointBefore(appPos))) matchedWordStarts += 1; // Increment the position in the query queryPos += Character.charCount(query.codePointAt(queryPos)); } else if (match) { if (matchPositions == null) matchPositions = new ArrayList<>(); matchPositions.add(Pair.create(beginMatch, normalizedAppPos)); match = false; } // If we are at the beginning of a word, add it to totalWordsStarts if (appPos == 0 || normalizedAppPos == 0 || Character.isUpperCase(pojo.name.codePointAt(appPos)) || Character.isWhitespace(pojo.name.codePointBefore(appPos))) totalWordStarts += 1; normalizedAppPos += Character.charCount(cApp); appPos = pojo.mapPosition(normalizedAppPos); } boolean matchedTags = false; if (match) { if (matchPositions == null) matchPositions = new ArrayList<>(); matchPositions.add(Pair.create(beginMatch, normalizedAppPos)); } int tagStart = 0; int tagEnd = 0; if (queryPos == query.length() && matchPositions != null) { // Add percentage of matched letters, but at a weight of 40 relevance += (int)(((double)queryPos / pojo.nameNormalized.length()) * 40); // Add percentage of matched upper case letters (start of word), but at a weight of 60 relevance += (int)(((double)matchedWordStarts / totalWordStarts) * 60); // The more fragmented the matches are, the less the result is important relevance *= (0.2 + 0.8 * (1.0 / matchPositions.size())); } else { if (pojo.tagsNormalized.startsWith(query)) { relevance = 4 + query.length(); } else if (pojo.tagsNormalized.indexOf(query) >= 0) { relevance = 3 + query.length(); } if (relevance > 0) { matchedTags = true; } tagStart = pojo.tagsNormalized.indexOf(query); tagEnd = tagStart + query.length(); } if (relevance > 0) { if (!matchedTags) { pojo.setDisplayNameHighlightRegion(matchPositions); } else { pojo.setTagHighlight(tagStart, tagEnd); } pojo.relevance = relevance; records.add(pojo); } } return records; } /** * Return a Pojo * * @param id we're looking for * @param allowSideEffect do we allow this function to have potential side effect? Set to false to ensure none. * @return an AppPojo, or null */ public Pojo findById(String id, Boolean allowSideEffect) { for (Pojo pojo : pojos) { if (pojo.id.equals(id)) { // Reset displayName to default value if (allowSideEffect) { pojo.displayName = pojo.name; if (pojo instanceof AppPojo) { AppPojo appPojo = (AppPojo)pojo; appPojo.displayTags = appPojo.tags; } } return pojo; } } return null; } public Pojo findById(String id) { return findById(id, true); } public Pojo findByName(String name) { for (Pojo pojo : pojos) { if (pojo.name.equals(name)) return pojo; } return null; } public ArrayList<Pojo> getAllApps() { ArrayList<Pojo> records = new ArrayList<>(pojos.size()); records.trimToSize(); for (AppPojo pojo : pojos) { pojo.displayName = pojo.name; pojo.displayTags = pojo.tags; records.add(pojo); } return records; } public void removeApp(AppPojo appPojo) { pojos.remove(appPojo); } }