package fr.neamar.kiss; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.SharedPreferences; import android.graphics.Bitmap.CompressFormat; import android.os.IBinder; import android.preference.PreferenceManager; import android.widget.Toast; import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import fr.neamar.kiss.dataprovider.AppProvider; import fr.neamar.kiss.dataprovider.ContactsProvider; import fr.neamar.kiss.dataprovider.IProvider; import fr.neamar.kiss.dataprovider.Provider; import fr.neamar.kiss.dataprovider.ShortcutsProvider; import fr.neamar.kiss.db.DBHelper; import fr.neamar.kiss.db.ShortcutRecord; import fr.neamar.kiss.db.ValuedHistoryRecord; import fr.neamar.kiss.pojo.Pojo; import fr.neamar.kiss.pojo.PojoComparator; import fr.neamar.kiss.pojo.ShortcutsPojo; import fr.neamar.kiss.utils.UserHandle; public class DataHandler extends BroadcastReceiver implements SharedPreferences.OnSharedPreferenceChangeListener { /** * List all known providers */ final static private List<String> PROVIDER_NAMES = Arrays.asList( "app", "contacts", "phone", "search", "settings", "shortcuts", "toggles" ); final private Context context; private String currentQuery; private Map<String, ProviderEntry> providers = new HashMap<>(); private boolean providersReady = false; private static TagsHandler tagsHandler; /** * Initialize all providers */ public DataHandler(Context context) { // Make sure we are in the context of the main activity // (otherwise we might receive an exception about broadcast listeners not being able // to bind to services) this.context = context.getApplicationContext(); IntentFilter intentFilter = new IntentFilter(MainActivity.LOAD_OVER); this.context.getApplicationContext().registerReceiver(this, intentFilter); Intent i = new Intent(MainActivity.START_LOAD); this.context.sendBroadcast(i); // Monitor changes for service preferences (to automatically start and stop services) SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); prefs.registerOnSharedPreferenceChangeListener(this); // Connect to initial providers for (String providerName : PROVIDER_NAMES) { if (prefs.getBoolean("enable-" + providerName, true)) { this.connectToProvider(providerName); } } } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (key.startsWith("enable-")) { String providerName = key.substring(7); if (PROVIDER_NAMES.contains(providerName)) { if (sharedPreferences.getBoolean(key, true)) { this.connectToProvider(providerName); } else { this.disconnectFromProvider(providerName); } } } } /** * Generate an intent that can be used to start or stop the given provider * * @param name The name of the provider * @return Android intent for this provider */ protected Intent providerName2Intent(String name) { // Build expected fully-qualified provider class name StringBuilder className = new StringBuilder(50); className.append("fr.neamar.kiss.dataprovider."); className.append(Character.toUpperCase(name.charAt(0))); className.append(name.substring(1).toLowerCase()); className.append("Provider"); // Try to create reflection class instance for class name try { return new Intent(this.context, Class.forName(className.toString())); } catch (ClassNotFoundException e) { e.printStackTrace(); return null; } } /** * Require the data handler to be connected to the data provider with the given name * * @param name Data provider name (i.e.: `ContactsProvider` → `"contacts"`) */ protected void connectToProvider(final String name) { // Do not continue if this provider has already been connected to if (this.providers.containsKey(name)) { return; } // Find provider class for the given service name Intent intent = this.providerName2Intent(name); if (intent == null) { return; } // Send "start service" command first so that the service can run independently // of the activity this.context.startService(intent); final ProviderEntry entry = new ProviderEntry(); // Connect and bind to provider service this.context.bindService(intent, new ServiceConnection() { @Override public void onServiceConnected(ComponentName className, IBinder service) { // We've bound to LocalService, cast the IBinder and get LocalService instance Provider.LocalBinder binder = (Provider.LocalBinder) service; IProvider provider = binder.getService(); // Update provider info so that it contains something useful entry.provider = provider; entry.connection = this; if (provider.isLoaded()) { handleProviderLoaded(); } } @Override public void onServiceDisconnected(ComponentName arg0) { } }, Context.BIND_AUTO_CREATE); // Add empty provider object to list of providers this.providers.put(name, entry); } /** * Terminate any connection between the data handler and the data provider with the given name * * @param name Data provider name (i.e.: `AppProvider` → `"app"`) */ protected void disconnectFromProvider(String name) { // Skip already disconnected services ProviderEntry entry = this.providers.get(name); if (entry == null) { return; } // Disconnect from provider service this.context.unbindService(entry.connection); // Stop provider service this.context.stopService(new Intent(this.context, entry.provider.getClass())); // Remove provider from list this.providers.remove(name); } /** * Called when some event occurred that makes us believe that all data providers * might be ready now */ private void handleProviderLoaded() { if (this.providersReady) { return; } // Make sure that all providers are fully connected for (ProviderEntry entry : this.providers.values()) { if (entry.provider == null || !entry.provider.isLoaded()) { return; } } // Broadcast the fact that the new providers list is ready try { this.context.unregisterReceiver(this); Intent i = new Intent(MainActivity.FULL_LOAD_OVER); this.context.sendBroadcast(i); } catch (IllegalArgumentException e) { // Nothing } this.providersReady = true; } @Override public void onReceive(Context context, Intent intent) { this.handleProviderLoaded(); } /** * Reload all currently used data providers */ public void reloadAll() { for (ProviderEntry entry : this.providers.values()) { if (entry.provider != null && entry.provider.isLoaded()) { entry.provider.reload(); } } } /** * Get records for this query. * * @param context android context * @param query query to run * @return ordered list of records */ public ArrayList<Pojo> getResults(Context context, String query) { query = query.toLowerCase().trim().replaceAll("<", "<"); currentQuery = query; // Have we ever made the same query and selected something ? List<ValuedHistoryRecord> lastIdsForQuery = DBHelper.getPreviousResultsForQuery( context, query); HashMap<String, Integer> knownIds = new HashMap<>(); for (ValuedHistoryRecord id : lastIdsForQuery) { knownIds.put(id.record, id.value); } // Ask all providers for data ArrayList<Pojo> allPojos = new ArrayList<>(); for (ProviderEntry entry : this.providers.values()) { if (entry.provider != null) { // Retrieve results for query: List<Pojo> pojos = entry.provider.getResults(query); // Add results to list for (Pojo pojo : pojos) { // Give a boost if item was previously selected for this query if (knownIds.containsKey(pojo.id)) { pojo.relevance += 25 * Math.min(5, knownIds.get(pojo.id)); } allPojos.add(pojo); } } } // Sort records according to relevance Collections.sort(allPojos, new PojoComparator()); return allPojos; } /** * Return previously selected items.<br /> * May return null if no items were ever selected (app first use)<br /> * May return an empty set if the providers are not done building records, * in this case it is probably a good idea to call this function 500ms after * * @param context android context * @param itemCount max number of items to retrieve, total number may be less (search or calls are not returned for instance) * @param smartHistory Recency vs Frecency * @param itemsToExclude Items to exclude from history * @return pojos in recent history */ public ArrayList<Pojo> getHistory(Context context, int itemCount, boolean smartHistory, ArrayList<Pojo> itemsToExclude) { // Pre-allocate array slots that are likely to be used based on the current maximum item // count ArrayList<Pojo> history = new ArrayList<>(Math.min(itemCount, 256)); // Read history List<ValuedHistoryRecord> ids = DBHelper.getHistory(context, itemCount, smartHistory); // Find associated items for (int i = 0; i < ids.size(); i++) { // Ask all providers if they know this id Pojo pojo = getPojo(ids.get(i).record); if (pojo != null) { //Look if the pojo should get excluded boolean exclude = false; for (int j = 0; j < itemsToExclude.size(); j++) { if (itemsToExclude.get(j).id.equals(pojo.id)) { exclude = true; break; } } if (!exclude) { history.add(pojo); } } } return history; } public int getHistoryLength() { return DBHelper.getHistoryLength(this.context); } public void addShortcut(ShortcutsPojo shortcut) { ShortcutRecord record = new ShortcutRecord(); record.name = shortcut.name; record.iconResource = shortcut.resourceName; record.packageName = shortcut.packageName; record.intentUri = shortcut.intentUri; if (shortcut.icon != null) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); shortcut.icon.compress(CompressFormat.PNG, 100, baos); record.icon_blob = baos.toByteArray(); } DBHelper.insertShortcut(this.context, record); if (this.getShortcutsProvider() != null) { this.getShortcutsProvider().reload(); } Toast.makeText(context, R.string.shortcut_added, Toast.LENGTH_SHORT).show(); } public void removeShortcut(ShortcutsPojo shortcut) { DBHelper.removeShortcut(this.context, shortcut.name); if (this.getShortcutsProvider() != null) { this.getShortcutsProvider().reload(); } } public void removeShortcuts(String packageName) { DBHelper.removeShortcuts(this.context, packageName); if (this.getShortcutsProvider() != null) { this.getShortcutsProvider().reload(); } } public void addToExcluded(String packageName, UserHandle user) { packageName = user.addUserSuffixToString(packageName, '#'); String excludedAppList = PreferenceManager.getDefaultSharedPreferences(context). getString("excluded-apps-list", context.getPackageName() + ";"); PreferenceManager.getDefaultSharedPreferences(context).edit() .putString("excluded-apps-list", excludedAppList + packageName + ";").apply(); } public void removeFromExcluded(String packageName, UserHandle user) { packageName = user.addUserSuffixToString(packageName, '#'); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.context); String excluded = prefs.getString("excluded-apps-list", context.getPackageName() + ";"); prefs.edit().putString("excluded-apps-list", excluded.replaceAll(packageName + ";", "")).apply(); } public void removeFromExcluded(UserHandle user) { // This is only intended for apps from foreign-profiles if(user.isCurrentUser()) { return; } SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.context); String[] excludedList = prefs.getString("excluded-apps-list", context.getPackageName() + ";").split(";"); StringBuilder excluded = new StringBuilder(); for(String excludedItem : excludedList) { if(!user.hasStringUserSuffix(excludedItem, '#')) { excluded.append(excludedItem + ";"); } } prefs.edit().putString("excluded-apps-list", excluded.toString()).apply(); } /** * Return all applications * * @return pojos for all applications */ public ArrayList<Pojo> getApplications() { return this.getAppProvider().getAllApps(); } public ContactsProvider getContactsProvider() { ProviderEntry entry = this.providers.get("contacts"); return (entry != null) ? ((ContactsProvider) entry.provider) : null; } public ShortcutsProvider getShortcutsProvider() { ProviderEntry entry = this.providers.get("shortcuts"); return (entry != null) ? ((ShortcutsProvider) entry.provider) : null; } public AppProvider getAppProvider() { ProviderEntry entry = this.providers.get("app"); return (entry != null) ? ((AppProvider) entry.provider) : null; } /** * Return most used items.<br /> * May return null if no items were ever selected (app first use) * * @param limit max number of items to retrieve. You may end with less items if favorites contains non existing items. * @return favorites' pojo */ public ArrayList<Pojo> getFavorites(int limit) { ArrayList<Pojo> favorites = new ArrayList<>(limit); String favApps = PreferenceManager.getDefaultSharedPreferences(this.context). getString("favorite-apps-list", ""); List<String> favAppsList = Arrays.asList(favApps.split(";")); // Find associated items for (int i = 0; i < favAppsList.size(); i++) { Pojo pojo = getPojo(favAppsList.get(i)); if (pojo != null) { favorites.add(pojo); } if (favorites.size() >= limit) { break; } } return favorites; } public boolean addToFavorites(MainActivity context, String id) { String favApps = PreferenceManager.getDefaultSharedPreferences(context). getString("favorite-apps-list", ""); // Check if we are already a fav icon if (favApps.contains(id + ";")) { //shouldn't happen return false; } List<String> favAppsList = Arrays.asList(favApps.split(";")); if (favAppsList.size() >= context.getFavIconsSize()) { favApps = favApps.substring(favApps.indexOf(";") + 1); } PreferenceManager.getDefaultSharedPreferences(context).edit() .putString("favorite-apps-list", favApps + id + ";").apply(); context.displayFavorites(); return true; } public boolean removeFromFavorites(MainActivity context, String id) { String favApps = PreferenceManager.getDefaultSharedPreferences(context). getString("favorite-apps-list", ""); // Check if we are not already a fav icon if (!favApps.contains(id + ";")) { //shouldn't happen return false; } PreferenceManager.getDefaultSharedPreferences(context).edit() .putString("favorite-apps-list", favApps.replace(id + ";", "")).apply(); context.displayFavorites(); return true; } public void removeFromFavorites(UserHandle user) { // This is only intended for apps from foreign-profiles if(user.isCurrentUser()) { return; } String[] favAppList = PreferenceManager.getDefaultSharedPreferences(this.context) .getString("favorite-apps-list", "").split(";"); StringBuilder favApps = new StringBuilder(); for(String favAppID : favAppList) { if(!favAppID.startsWith("app://") || !user.hasStringUserSuffix(favAppID, '/')) { favApps.append(favAppID + ";"); } } PreferenceManager.getDefaultSharedPreferences(this.context).edit() .putString("favorite-apps-list", favApps.toString()).apply(); } /** * Insert specified ID (probably a pojo.id) into history * * @param id pojo.id of item to record */ public void addToHistory(String id) { boolean frozen = PreferenceManager.getDefaultSharedPreferences(context). getBoolean("freeze-history", false); if (!frozen) { DBHelper.insertHistory(this.context, currentQuery, id); } } private Pojo getPojo(String id) { // Ask all providers if they know this id for (ProviderEntry entry : this.providers.values()) { if (entry.provider != null && entry.provider.mayFindById(id)) { return entry.provider.findById(id); } } return null; } protected class ProviderEntry { public IProvider provider = null; public ServiceConnection connection = null; } public TagsHandler getTagsHandler() { if (tagsHandler == null) { tagsHandler = new TagsHandler(context); } return tagsHandler; } public void resetTagsHandler() { tagsHandler = new TagsHandler(this.context); } }