Differences between `mActive` and `shouldBeActive()` in `ObserverWrapper` - android

I'm recently learning Android LiveData and its observer pattern. But I am confused by the meaning of ObserverWrapper.shouldBeActive() when reading LiveData.considerNotify():
private void considerNotify(ObserverWrapper observer) {
if (!observer.mActive) {
return;
}
// Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
//
// we still first check observer.active to keep it as the entrance for events. So even if
// the observer moved to an active state, if we've not received that event, we better not
// notify for a more predictable notification order.
if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
if (observer.mLastVersion >= mVersion) {
return;
}
observer.mLastVersion = mVersion;
//noinspection unchecked
observer.mObserver.onChanged((T) mData);
}
I would like to know the differences between mActive and shouldBeActive(), and what is the purpose of implementing the latter function. Thank you.

Related

Suspending a Kotlin coroutine until a flow has a specific value

I am currently playing around with Kotlin coroutines and flows. In my scenario, a MutableStateFlow represents a connection state (CONNECTING, CONNECTED, CLOSING, CLOSED). It is also possible to login, logout and login again.
For further use of the connection, I have to check the state and wait until it is CONNECTED. If it is already CONNECTED, I can continue. If not, I have to wait until the state reaches CONNECTED. The connect() call does return immediately, the result is propagated via a callback that updates the MutableStateFlow. My current idea is to do the following:
connect()
if (connectionState.value != State.CONNECTED) { // connectionState = MutableStateFlow(State.CLOSED)
suspendCoroutine<Boolean> { continuation ->
scope.launch { // scope = MainScope()
connectionState.collect {
if (it == State.CONNECTED) {
continuation.resume(true)
}
}
}
}
}
// continue
As I am fairly new to the topic, I don't know if this is good practice and I was also not able to find a more suitable concept in the Kotlin documenation. Is there some better way of doing it?
A while back I had the same question:
It is preferred to use first() to suspend till the predicate is matched.
if (connectionState.value != State.CONNECTED) {
connectionState.first { it == State.CONNECTED }
}

How to implement back button handling on Android with Redux

I'm trying to implement back button handling on Android using CoRedux for my Redux store. I did find one way to do it, but I am hoping there is a more elegant solution because mine seems like a hack.
Problem
At the heart of the problem is the fact returning to an Android Fragment is not the same as rendering that Fragment for the first time.
The first time a user visits the Fragment, I render it with the FragmentManager as a transaction, adding a back stack entry for the "main" screen
fragmentManager?.beginTransaction()
?.add(R.id.myFragmentContainer, MyFragment1())
?.addToBackStack("main")?.commit()
When the user returns to that fragment from another fragment, the way to get back to it is to pop the back stack:
fragmentManager?.popBackStack()
This seems to conflict with Redux principles wherein the state should be enough to render the UI but in this case the path TO the state also matters.
Hack Solution
I'm hoping someone can improve on this solution, but I managed to solve this problem by introducing some state that resides outside of Redux, a boolean called skipRendering. You could call this "ephemeral" state perhaps. Initialized to false, skipRendering gets set to true when the user taps the back button:
fun popBackStack() {
fragmentManager?.popBackStack()
mapViewModel.dispatchAction(MapViewModel.ReduxAction.BackButton)
skipRendering = true
}
Dispatching the back button to the redux store rewinds the redux state to the prior state as follows:
return when (action) {
// ...
ReduxAction.BackButton -> {
state.pastState
?: throw IllegalStateException("More back taps processed than past state frames")
}
}
For what it's worth, pastState gets populated by the reducer whenever the user requests to visit a fragment from which the user can subsequently tap back.
return when (action) {
// ...
ReduxAction.ShowMyFragment1 -> {
state.copy(pastState = state, screenDisplayed = C)
}
}
Finally, the render skips processing if skipRendering since the necessary work of calling fragmentManager?.popBackStack() was handled before dispatching the BackButton action.
I suspect there is a better solution which uses Redux constructs for example a side effect. But I'm stuck figuring out a way to solve this more elegantly.
To solve this problem, I decided to accept that the conflict cannot be resolved directly. The conflict is between Redux and Android's native back button handling because Redux needs to be master of the state but Android holds the back stack information. Recognizing that these two don't mix well, I decided to ditch Android's back stack handling and implement it entirely on my Redux store.
data class LLReduxState(
// ...
val screenBackStack: List<ScreenDisplayed> = listOf(ScreenDisplayed.MainScreen)
)
sealed class ScreenDisplayed {
object MainScreen : ScreenDisplayed()
object AScreen : ScreenDisplayed()
object BScreen : ScreenDisplayed()
object CScreen : ScreenDisplayed()
}
Here's what the reducer looks like:
private fun reducer(state: LLReduxState, action: ReduxAction): LLReduxState {
return when (action) {
// ...
ReduxAction.BackButton -> {
state.copy(screenBackStack = mutableListOf<ScreenDisplayed>().also {
it.addAll(state.screenBackStack)
it.removeAt(0)
})
}
ReduxAction.AButton -> {
state.copy(screenBackStack = mutableListOf<ScreenDisplayed>().also {
it.add(ScreenDisplayed.AScreen)
it.addAll(state.screenBackStack)
})
}
ReduxAction.BButton -> {
state.copy(screenBackStack = mutableListOf<ScreenDisplayed>().also {
it.add(ScreenDisplayed.BScreen)
it.addAll(state.screenBackStack)
})
}
ReduxAction.CButton -> {
state.copy(screenBackStack = mutableListOf<ScreenDisplayed>().also {
it.add(ScreenDisplayed.CScreen)
it.addAll(state.screenBackStack)
})
}
}
}
In my fragment, the Activity can call this API I exposed when the Activity's onBackPressed() gets called by the operating system:
fun popBackStack() {
mapViewModel.dispatchAction(MapViewModel.ReduxAction.BackButton)
}
Lastly, the Fragment renders as follows:
private fun render(state: LLReduxState) {
// ...
if (ScreenDisplayed.AScreen == state.screenBackStack[0]) {
fragmentManager?.beginTransaction()
?.replace(R.id.llNavigationFragmentContainer, AFragment())
?.commit()
}
if (ScreenDisplayed.BScreen == state.screenBackStack[0]) {
fragmentManager?.beginTransaction()
?.replace(R.id.llNavigationFragmentContainer, BFragment())
?.commit()
}
if (ScreenDisplayed.CScreen == state.screenBackStack[0]) {
fragmentManager?.beginTransaction()
?.replace(R.id.llNavigationFragmentContainer, CFragment())
?.commit()
}
}
This solution works perfectly for back button handling because it applies Redux in the way it was meant to be applied. As evidence, I was able to write automation tests which mock the back stack as follows by setting the initial state to one with the deepest back stack:
LLReduxState(
screenBackStack = listOf(
ScreenDisplayed.CScreen,
ScreenDisplayed.BScreen,
ScreenDisplayed.AScreen,
ScreenDisplayed.MainScreen
)
)
I've left some details out which are specific to CoRedux.

Why does Android AppOps (privacy manager) send duplicate events when user change his/her privacy settings?

When a user changes his/her privacy settings through AppOps (e.g. denying an application access to phone contacts), AppOpsManager sends to anyone who listens what the users have changed (i.e. the package name and the operation (e.g. Read contacts)).
So I wrote a listener to do so. However, we the user make only one change, I receive too many duplicate events (e.g. 10 events that the user decided to deny Angry Bird access to his/her location) and then the app crashes.
Here is my code to register listners for each pair of package & operation:
public void startWatchingOperations(AppOpsManager appOps, List<AppOpsManager.PackageOps> opsforapps) {
SharedPreferences myAppListnerPreferences = getSharedPreferences(APP_OPS_PREFERENCES, Activity.MODE_PRIVATE);
for (AppOpsManager.PackageOps o:opsforapps) {
List<OpEntry> opEntry = o.getOps();
//if I already assigned a listener to this pari of package & operation, then skip
if (myAppListnerPreferences.getBoolean(o.getPackageName(), false)==false) {
for (OpEntry entry:opEntry) {
//for each pair of package & operation, assign a new listener
ChangePrivacySettingsListener opsListner = new ChangePrivacySettingsListener(getApplicationContext());
appOps.startWatchingMode(entry.getOp(),o.getPackageName(),opsListner);
}
myAppListnerPreferences.edit().putBoolean(o.getPackageName(), true).apply();
}
}
}
Here is a snippet of the listener
public class ChangePrivacySettingsListener implements AppOpsManager.Callback {
public void opChanged(int op, String packageName) {
AppOpsManager appOps= (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE);
PackageManager pkg = context.getPackageManager();
try {
//this is an object to store the event: package name,
// the operation that has been changed, & time stamp
PrivacySetting privacySetting = new PrivacySetting();
privacySetting.setPackageName(packageName);
privacySetting.setOperation(OPERATIONS_STRINGS[op]);
privacySetting.setDecisionTime(Calendar.getInstance(TimeZone.getDefault()).getTimeInMillis());
privacySetting.setUserId(userId);
} catch (NameNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
Her is the part of AppOpsManager.java that allows me to listen to user's changes.
public class AppOpsManager {
final HashMap<Callback, IAppOpsCallback> mModeWatchers
= new HashMap<Callback, IAppOpsCallback>();
public void startWatchingMode(int op, String packageName, final Callback callback) {
synchronized (mModeWatchers) {
IAppOpsCallback cb = mModeWatchers.get(callback);
if (cb == null) {
cb = new IAppOpsCallback.Stub() {
public void opChanged(int op, String packageName) {
callback.opChanged(op, packageName);
}
};
mModeWatchers.put(callback, cb);
}
try {
mService.startWatchingMode(op, packageName, cb);
} catch (RemoteException e) {
}
}
}
I double checked to ensure that I've never assigned more than one listener to each pair of package & operation.
I would appreciate hints about potential causes.
Here is a link to AppOpsManager.java
Try moving the deceleration of ChangePrivacySettingsListener opsListner to be out side of the for block:
public void startWatchingOperations(AppOpsManager appOps, List<AppOpsManager.PackageOps> opsforapps) {
ChangePrivacySettingsListener opsListner;
SharedPreferences myAppListnerPreferences = getSharedPreferences(APP_OPS_PREFERENCES, Activity.MODE_PRIVATE);
for (AppOpsManager.PackageOps o:opsforapps) {
List<OpEntry> opEntry = o.getOps();
//if I already assigned a listener to this pari of package & operation, then skip
if (myAppListnerPreferences.getBoolean(o.getPackageName(), false)==false) {
for (OpEntry entry:opEntry) {
//for each pair of package & operation, assign a new listener
opsListner = new ChangePrivacySettingsListener(getApplicationContext());
appOps.startWatchingMode(entry.getOp(),o.getPackageName(),opsListner);
}
myAppListnerPreferences.edit().putBoolean(o.getPackageName(), true).apply();
}
}
}
And please let me know what happened?
Just in case this is helpful to someone, up to at least Android Oreo, calling AppOpsManager.startWatchingMode(op, packageName, callback) will cause callback to be invoked when the setting is changed (1) for the op with any package, AND (2) for any AppOps setting changes with packageName. This can be seen from the AppOpsService.java source, particularly AppOpsService.startWatchingMode() which registers the callback, AppOpsService.setMode() which calls the callback when the AppOps setting is changed.
For example, if you register a callback with startWatchingMode(appOps1, package1, callback) and startWatchingMode(appOps2, package1, callback),
when there is a change in the setting for appOps3 for package1, the callback will be called twice since you have registered for package1 two times. If there is a change in appOps1 for package1, the callback will be invoked 3 times, because you have registered once for appOps1, and twice for package1.
The solution is to register either for the set of AppOps you are interested in (without duplications), with the packageName parameter set to null, or register for the set of packages you are interested in, with op parameter set to AppOpsManager.OP_NONE.
Also you need to ensure that all listeners are unregistered (e.g. in onDestroy of your activity) using stopWatchingMode. Otherwise, the callback entries will accumulate across Activity lifecycles (until the app is terminated) and you will start getting duplicates. This also means that you should keep references to all the listeners created.

Is there way to make talkback speak?

While making my application accessible, I have a problem - there's no way to make it SPEAK!!
By referencing google's library, I make
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event)
on my customized view and I get right event message - I checked it by using Log.d
However, there's no way to make talkback to speak...
My Application runs from API8 so I can't use also,
onPopulateAccessibilityEvent()
Am I thinking wrong? Please somebody help me...
For people looking to implement #Carter Hudson's code in Java (don't judge me cause I'm still not using Kotlin in 2019):
AccessibilityManager accessibilityManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
AccessibilityEvent accessibilityEvent = AccessibilityEvent.obtain();
accessibilityEvent.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT);
accessibilityEvent.getText().add("Text to be spoken by TalkBack");
if (accessibilityManager != null) {
accessibilityManager.sendAccessibilityEvent(accessibilityEvent);
}
I needed to announce when a button became visible after reloading a RecyclerView's items with a new dataset. RecyclerView being a framework view, it supports talkback / accessibility out-of-the-box. After loading new data, talkback announces "showing items x through y of z" automatically. Utilizing the TTS API to solve the use case I mentioned introduces the following pitfalls:
TTS instance initialization and management is cumbersome and questionable for the following reasons:
Managing TTS instance lifecycle with onInit listener
Managing Locale settings
Managing resources via shutdown() ties you to an Activity's lifecycle per documentation
An Activity's onDestroy is not guaranteed to be called, which seems like a poor mechanism for calling shutdown() in order to deallocate TTS resources.
An easier, more maintainable solution is to play nicely with TalkBack and utilize the Accessibility API like so:
class AccessibilityHelper {
companion object {
#JvmStatic
fun announceForAccessibility(context: Context, announcement: String) {
context
.getSystemService(ACCESSIBILITY_SERVICE)
.let { it as AccessibilityManager }
.let { manager ->
AccessibilityEvent
.obtain()
.apply {
eventType = TYPE_ANNOUNCEMENT
className = context.javaClass.name
packageName = context.packageName
text.add(announcement)
}
.let {
manager.sendAccessibilityEvent(it)
}
}
}
}
}
Call the above from wherever you need (I added a method to my base activity that forwards to the helper). This will insert the announcement into the queue of messages for TalkBack to announce out loud and requires no handling of TTS instances. I ended up adding a delay parameter and mechanism into my final implementation to separate these events from ongoing ui-triggered events as they sometimes tend to override manual announcements.
Very this is tool, can use it everywhere with guard
public static void speak_loud(String str_speak) {
if (isGoogleTalkbackActive()) {
AccessibilityManager accessibilityManager = (AccessibilityManager) getDefaultContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
AccessibilityEvent accessibilityEvent = AccessibilityEvent.obtain();
accessibilityEvent.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT);
accessibilityEvent.getText().add(str_speak);
if (accessibilityManager != null) {
accessibilityManager.sendAccessibilityEvent(accessibilityEvent);
}
}
}
public static boolean isGoogleTalkbackActive() {
AccessibilityManager am = (AccessibilityManager) getDefaultContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
if (am != null && am.isEnabled()) {
List<AccessibilityServiceInfo> serviceInfoList = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_SPOKEN);
if (!serviceInfoList.isEmpty())
return true;
}
return false;
}
If you want it to speak, use the TextToSpeech API. It takes a string and reads it outloud.
announceForAccessibility method defined in the View class probably serves the purpose here. It was introduced in API level 16. More details here.

How to know if my SensorManager has a registered Sensor

I'm using a sensor for my Android Application. I register the sensor with a line of code:
mySensorManager.registerListener(this, orientationSensor, SensorManager.SENSOR_DELAY_NORMAL);
I have introduced a line of code to unregister the listener so it wont be running all time:
mySensorManager.unregisterListener(this);
So far it works, but i need to register it again when the application resumes. I need to know if my sensorManager has a registered listener so I can registered again or skit it. Does something like this can be done?:
if (mySensorManager.getRegisteredListenerList == null){ registerListener() } else { .. }
As far as I know, there is no native way to check if you have already registered listener. I would not worry too much about this check since this check is already done by Android, so if you have already registered listener, Android is not gonna add the same listener twice:
#SuppressWarnings("deprecation")
private boolean registerLegacyListener(int legacyType, int type,
SensorListener listener, int sensors, int rate)
{
if (listener == null) {
return false;
}
boolean result = false;
// Are we activating this legacy sensor?
if ((sensors & legacyType) != 0) {
// if so, find a suitable Sensor
Sensor sensor = getDefaultSensor(type);
if (sensor != null) {
// If we don't already have one, create a LegacyListener
// to wrap this listener and process the events as
// they are expected by legacy apps.
LegacyListener legacyListener = null;
synchronized (mLegacyListenersMap) {
legacyListener = mLegacyListenersMap.get(listener);
if (legacyListener == null) {
// we didn't find a LegacyListener for this client,
// create one, and put it in our list.
legacyListener = new LegacyListener(listener);
mLegacyListenersMap.put(listener, legacyListener);
}
}
// register this legacy sensor with this legacy listener
legacyListener.registerSensor(legacyType);
// and finally, register the legacy listener with the new apis
result = registerListener(legacyListener, sensor, rate);
}
}
return result;
}
So you can call registerListener as many times as you want it will only be added once:)
The same is applicable for unregister logic
#addition to Pavel:
SensorEventListeners can be registered multiple times. Seen here and in Java Programming for Android Developers. I did register 9 Sensors with Listener delay of SENSOR_DELAY_GAME and run() with THREAD_PRIORITY_BACKGROUND. It works fine. To unregister I did the following in private class readoutRunnable implements Runnable:
#Override
public void run() {
try {
//process events from locally stored SensorEvent[]
// array is filled from onSensorChanged()
} catch (Exception e) {
// catch e
} finally {
sensorManager.unregisterListener(listener);
}
}
This puts lots of events in the onsensorchanged() which is why you should take googles advice to not block the method all the more serious:
Sensor data can change at a high rate, which means the system may call the onSensorChanged(SensorEvent) method quite often. As a best practice, you should do as little as possible within the onSensorChanged(SensorEvent) method so you don't block it. If your application requires you to do any data filtering or reduction of sensor data, you should perform that work outside of the onSensorChanged(SensorEvent) method.

Categories

Resources