android: "progressive" custom search provider - android

I have a custom search content provider that combines recent searches and specific results from making a network REST call.
The problem is that if the network hangs, or is slow, no results come back. I'm wondering if there's a way to progressively return results ... in my case, return previous searches immediately, and return network search results when they are available.
I'm not seeing how this would be possible, since this appears to be a pull model. the UI component is requesting the search results, and has no way to know when to re-request to obtain the network based results ... that's even if I could understand to hook into it to make it re-request the results.
Any ideas?

This is how I went about handling this problem. the content provider itself never accesses the network in the main request thread. in stead, it,
looks for a cached result to the query
if there is a cached result, return that immediately
if there is no cached result, start a worker thread to find the result, and return empty results
when the worker thread from #3 is complete, it broadcasts an intent. this is received by the activity hosting the search, triggering it to re-submit the same search. this time when the search query gets to the content provider, the result is cached, and returned immediately.
the only complexity to this was how to force the activity to re-submit the search without user interaction. the solution was different depending on the version of android,
public void onReceive(Context context, Intent intent) {
String query = intent.getStringExtra(SearchManager.QUERY);
if (query == null) {
return;
}
// if user has typed something new, ignore
if (!query.equals(searchQuery)) {
return;
}
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB) {
SearchView searchView = (SearchView) searchMenuItem.getActionView();
searchView.setQuery(query, false);
} else {
SearchManager sm = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
ComponentName cm = FolderActivity.this.getComponentName();
sm.startSearch(query, false, cm, null, false);
}
}

Related

How to get ViewModel observer to not observe initially?

I have a SearchView icon on the Toolbar via onCreateOptionsMenu(Menu menu). A click on the icon creates an EditText line for the user to enter search inputs. I have a TextWatcher() attached to the EditText line so that when text is added to the line (afterTextChanged Editable s) a searchQuery method is run via the ViewModel that searches a Room database.
My problem is that the observer is firing upon creation even before the user enters any query. Using Toasts, I was able to confirm that the observer is running a blank query "%%" right when the SearchView icon is pressed by the user and before the user enters any search query. And this is happening even though the observer is set up in the afterTextChanged(). How do I get the ViewModel observer to only fire after text is entered by the user?
Activity
...
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.mainactiv_menu, menu);
searchItem = menu.findItem(R.id.action_search);
menu.findItem(R.id.action_search).setVisible(false);
if (cardsAdapter != null && cardsAdapter.getItemCount() > 0) {
menu.findItem(R.id.action_search).setVisible(true);
}
SearchManager searchManager = (SearchManager) MainActivity.this.getSystemService(Context.SEARCH_SERVICE);
if (searchItem != null) {
mSearchView = (SearchView) searchItem.getActionView();
if (mSearchView != null) {
mSearchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
EditText mSearchEditText = mSearchView.findViewById(androidx.appcompat.R.id.search_src_text);
mSearchEditText.setInputType(android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
mSearchEditText.addTextChangedListener(new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// not needed
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// not needed
}
#Override
public void afterTextChanged(Editable s) {
String queryText = "%" + s.toString() + "%";
mQuickcardViewModel.searchQuery(queryText).observe(MainActivity.this, searchCards -> {
searchList = searchCards;
if (!mSearchView.isIconified() && searchList.size() > 0) {
// do something
}
...
How do I get the ViewModel observer to only fire after text is entered by the user?
Use an if statement to see if the string is empty before observing:
#Override
public void afterTextChanged(Editable s) {
if (s.toString().length() > 0) {
// the rest of your code goes here, preferably with some modifications
}
}
Other changes that I would recommend include:
Adding some measure of "debounce". Given your search expression, you appear to be hitting a database in searchQuery(). If the user types 10 characters in rapid succession, you will make 10 queries, which will make performance poor. "Debounce" says "only query after the user has paused for a bit", such as 500ms without any further input.
Adding in smarts to cancel an outstanding query when needed. Suppose the user types a bit, and then the debounce period elapses. So, you fire off a query. Because of your (presumed) LIKE expression, your query is going to do a "table scan", examining every row of the table. That could be slow, and the user might start typing some more before that first query completes. You no longer need that query, as its results are wrong (it is for the previous search expression, not the current one). So, you need a way to cancel that work.
Dealing with configuration changes. You might argue that you are using a ViewModel and LiveData, which should handle that... and it does, but only if you use them properly. In your case, you appear to be throwing away that LiveData after observing it, and so on a configuration change, you will not be in position re-observe it. This is why a lot of samples focus on decoupling the "please do the background work" from "please give me the results of the background work". You could still have a searchQuery() method that you call, but it would return void. The results would get piped through some LiveData held by the ViewModel, one that you observe in onCreate(). That way, after a configuration change, you once again observe that LiveData, and you get the data as it was prior to that configuration change.
Using FTS. LIKE is slow. If you are going to do this a lot consider using FTS3/FTS4 in SQLite for full-text searching. If you are using Room, there is built-in support for this in Room 2.2.0.

How to utilize Android Nougat's Direct Reply feature with a NotificationListener?

My app is using a NotificationListener to read out messages from various 3rd party apps, for example WhatsApp.
So far I was able to send a reply if only one chat is unread, the code is below.
However, in the case with WhatsApp, getNotification().actions returns a null object when more than two chats are unread, as the messages are bundled together. As you can see in the pictures below, if the notifications are extended there is an option to send a direct reply as well, therefore I am certain that it is possible to utilize this, also I think apps like PushBullet are using this method.
How could I access the RemoteInput of that notification?
public static ReplyIntentSender sendReply(StatusBarNotification statusBarNotification, String name) {
Notification.Action actions[] = statusBarNotification.getNotification().actions;
for (Notification.Action act : actions) {
if (act != null && act.getRemoteInputs() != null) {
if (act.title.toString().contains(name)) {
if (act.getRemoteInputs() != null)
return new ReplyIntentSender(act);
}
}
}
return null;
}
public static class ReplyIntentSender {
[...]
public final Notification.Action action;
public ReplyIntentSender(Notification.Action extractedAction) {
action = extractedAction;
[...]
}
private boolean sendNativeIntent(Context context, String message) {
for (android.app.RemoteInput rem : action.getRemoteInputs()) {
Intent intent = new Intent();
Bundle bundle = new Bundle();
bundle.putCharSequence(rem.getResultKey(), message);
android.app.RemoteInput.addResultsToIntent(action.getRemoteInputs(), intent, bundle);
try {
action.actionIntent.send(context, 0, intent);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
return false;
}
}
Some explanation how the above code works: Once a notification is received the app tries to get the actions and checks if the name is in the title of a remoteInput (normally it is in the format of "Reply to $NAME"), if that is found the Action is saved into a ReplyIntentSender class, which, when triggered by sendNativeIntent, cycles through all RemoteInputs of that Action and adds the message to the intent. If more than one chat is unread, getNotification().actions returns null.
Below are two screenshots, the first one where it is working without any problems and the second one where it doesn't.
You can consider this as my suggestion. I have done bit research on this and come up with following conclusions.(Also it looks like you have done plenty of research on this so it might be possible that you aware about what I wrote below)
Numerous apps send Wear specific notifications, and many of those contain actions accessible from an Android Wear device. We can grab those Wear notifications on the device, extracting the actions, finding the reply action (if one exists), populating it with our own response and then executing the PendingIntent which sends our response back the original app for it to send on to the recipient.
To do so you can refer this link (A nice workaround by Rob J). You can also refer this link in this context (Great research work done by Michał Tajchert).(You might need to work around with NotificationCompat.isGroupSummary)
This is what I feel(Might be I am totally wrong)
.actions method returns Array of all Notification.Action
structures attached to current notification by addAction(int,
CharSequence, PendingIntent), Here addAction method is deprecated
one so it might not working as intended.
I am not able to test this at my end otherwise I will love to provide a working solution with code.
Hope this will help you. Happy Coding!!!

StreetViewPanorama().getLocation() inconsistent results?

My intent is to show the fragment if there is something to show and hide it if there isn't.
My problem is that, given the same coordinates:
On first attempt, mStreetView.getStreetViewPanorama().getLocation() has a non-null response and shows the view. However, the Fragment is black (blank).
On second attempt, mStreetView.getStreetViewPanorama().getLocation() has a null response and the view stays hidden.
My code:
mStreetView.getView().setVisibility(View.GONE);
mStreetView.getStreetViewPanorama().setPosition(customMarker.getPosition());
if (mStreetView.getStreetViewPanorama().getLocation() != null &&
mStreetView.getStreetViewPanorama().getLocation().links != null) {
mStreetView.getView().setVisibility(View.VISIBLE);
}
I'm not sure how to go about debugging this. It seems to me that the results shouldn't vary like this, especially since, even though it has non-null results, the results have (apparently) no valid value to allow something to be displayed.
Edit:
This coordinate functions as expected and shows the view properly populated: 33.6645598,-111.9253126
This coordinate shows the view, but it is black, then later returns null: 33.6492448,-111.9354228
These results are consistent.
Edit2:
I attempted to use the OnStreetViewPanoramaReadyCallback(), however the results were the same.
Code is now:
mStreetView.getView().setVisibility(View.GONE);
mStreetView.getStreetViewPanoramaAsync(new OnStreetViewPanoramaReadyCallback() {
#Override
public void onStreetViewPanoramaReady(StreetViewPanorama streetViewPanorama) {
streetViewPanorama.setPosition(customMarker.getPosition());
if (streetViewPanorama.getLocation() != null && streetViewPanorama.getLocation().links != null) {
mStreetView.getView().setVisibility(View.VISIBLE);
}
}
});
In one of my old projects found there is a race condition if you try to access the location too early
what I did was setup a handler and have a post delayed runnable fire after 1000 milliseconds then check the location. Doing this provided consistent results.
I dont know if this is still the current behavior but you can read what I did here
Android StreetView check if there is any view for given location
Edit
there appears to be a onStreetViewPanoramaReady callback now, are you using that?
If you use the 'getLocation()' before the view has been created, it will return null. It is recommended to wait until view has been created. Also, you have to create a callback to let you know when the streetview is ready.
getStreetViewPanoramaAsync(new OnStreetViewPanoramaReadyCallback(){
#Override
public void onStreetViewPanoramaReady(StreetViewPanorama streetViewPanorama) {
}
})

How would a thread created by an app be considered a different app from the app's ContentProvider?

I have an app that, when notified by a ContentObserver of a change to a ContentProvider, attempts to query the provider on a background thread. This causes an SecurityException to be thrown:
8-10 15:54:29.577 3057-3200/com.xxxx.mobile.android.xxx W/Binder﹕ Caught a RuntimeException from the binder stub implementation.
java.lang.SecurityException: Permission Denial: reading com.xxx.mobile.android.mdk.model.customer.ContentProvider uri content://com.xxx.mobile.android.consumer.xxx/vehicle from pid=0, uid=1000 requires the provider be exported, or grantUriPermission()
at android.content.ContentProvider.enforceReadPermissionInner(ContentProvider.java:539)
at android.content.ContentProvider$Transport.enforceReadPermission(ContentProvider.java:452)
at android.content.ContentProvider$Transport.query(ContentProvider.java:205)
at android.content.ContentResolver.query(ContentResolver.java:478)
at android.content.ContentResolver.query(ContentResolver.java:422)
How would a thread created by an app end up with a different UID from the app's ContentProvider?
By placing an exception breakpoint in android.content.ContentProvider I see that UserHandle.isSameApp(uid, mMyUid) is false and UserHandle.isSameUser(uid, mMyUid) is true. I also see that the providers UID is 10087.
The uid value of 1000 belongs to the Android system. Many features of Android involve proxying requests to the system thread for processing. If an exception is thrown during this, the error will include the uid of the system, rather than the original requestor.
For the other points:
UserHandle.isSameApp(uid, mMyUid) is false
UserHandle.isSameUser(uid, mMyUid) is true
These are easiest to explain by looking at the source. On an Android device with multi-user support, each user is defined by a range of UIDs. isSameApp is false because the modulus of the ids do not match:
public static final boolean isSameApp(int uid1, int uid2) {
return getAppId(uid1) == getAppId(uid2);
}
public static final int getAppId(int uid) {
return uid % PER_USER_RANGE;
}
Similarly, the two ids belong to the same user because they live in the same range:
public static final boolean isSameUser(int uid1, int uid2) {
return getUserId(uid1) == getUserId(uid2);
}
public static final int getUserId(int uid) {
if (MU_ENABLED) {
return uid / PER_USER_RANGE;
} else {
return 0;
}
}
Note that this logic is flawed because it means that all Android system uids (< 10000) will be assumed to "belong" to the first user.
Also note that if a second user installs more than 1000 apps(!), there's the possibility that an App will be mistaken for a system app (both uid % PER_USER_RANGE will return 1000). It won't really matter though, because the strong sandboxing would prevent anything too bad from happening.
I got the same problem while trying to interact with my ContentProvider in a system callback (LeScanCallback). The problem is that the callback thread is owned by the Android system, and not by my app, even if the code is in my app.
Passing the work from the callback to one of my app threads before trying to interact with my ContentProvider solved the problem successfully.
To reduce boilerplate for thread creation and recycling (needed for frequent callbacks to reduce overhead), I used AndroidAnnotation's #Background annotation on my delegate method (but would use Kotlin Coroutines today).
If a Thread is started by any component of the application that has the provider, then you can access the ContentProvider without any SecurityException.
I am using a ContentProvider in my application just as an extra abstraction layer and I haven't exposed the content to other applications. I am accessing the ContentProvider in background thread (Not AsyncTask, but a simple java.lang.Thread). I am not getting any SecurityException. The following is the code from my application.
AndroidManifest.xml
<provider
android:authorities="com.sample.provider"
android:name="com.sample.MyProvider"
android:exported="false" />
MainActivity
public void performContinue(Bundle extras){
Thread thread = new Thread(new Runnable() {
#Override
public void run() {
String AUTHORITY = "com.sample.provider";
Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
Uri currentUri = BASE_URI.buildUpon().appendPath("SAMPLE_COUNT").build();
final Cursor query = InputActivity.this.getContentResolver().query(currentUri, null, null, null, null);
if (query != null) {
final int count = query.getCount();
Log.d("DEBUG","CONTENT = " + count);
}else{
Log.d("DEBUG","CONTENT = CURSOR NULL");
}
}
});
thread.setName("THREAD_1");
thread.start();
}
I don't seem to get any SecurityException. Ideally we need to be using AsyncQueryHandler for accessing the ContentProvider, because that allows you do all the fetching process in the background thread and will use the UI Thread to post the results in the UI. But after seeing this post, I just wanted to see if I could just use Thread and check if I could still access it without any Exception. It works fine.

Is Android system service listener localized to my app instance?

I am writing a Spell Check client using the sample code in the SDK as an example. I have the following code (not actual implementation, but an accurate sample representation):
public class HelloSpellChecker implements SpellCheckerSessionListener {
private SpellCheckerSession mSpellCheckService;
private void bindService() {
final TextServicesManager tsm = (TextServicesManager) getSystemService(
Context.TEXT_SERVICES_MANAGER_SERVICE);
mSpellCheckService = tsm.newSpellCheckerSession(null, null, this, true);
}
public void getSuggestions(String word) {
mSpellCheckService.getSuggestions(new TextInfo("tgis"), 3);
}
#Override
public void onGetSentenceSuggestions(final SentenceSuggestionsInfo[] arg0) {
Log.d(TAG, "onGetSentenceSuggestions");
// Process suggestions
}
}
What I want to know is will onGetSentenceSuggestions only be fired when my application calls getSuggestions, or will it be fired any time the system service receives a request to getSuggestions?
If it is the latter, what is the best way to ensure my app only processes suggestions which it requested?
I would say Android system service listener is localized through the session in this case.
onGetSentenceSuggestions method is fired by any request to getSuggestions method. However, you don't have to worry about processing suggestions which your app requested since the spellchecker session takes care of it. Your app only gets the suggestions requested by the session your app created to interact with the spell checker service.
Hope this helps.
References:
http://developer.android.com/reference/android/view/textservice/SpellCheckerSession.html
http://developer.android.com/guide/topics/text/spell-checker-framework.html

Categories

Resources