e-mission / e-mission-docs

Repository for docs and issues. If you need help, please file an issue here. Public conversations are better for open source projects than private email.
https://e-mission.readthedocs.io/en/latest
BSD 3-Clause "New" or "Revised" License
15 stars 32 forks source link

⬆️ Upgrade to latest everything 🎉 #680

Closed shankari closed 2 years ago

shankari commented 2 years ago

At long last, after clearing the decks with https://github.com/e-mission/e-mission-phone/pull/799 https://github.com/e-mission/e-mission-phone/pull/800 https://github.com/e-mission/e-mission-phone/pull/801 https://github.com/e-mission/e-mission-phone/pull/802

I'm ready to start the annual ritual of upgrading to the most recent version of everything. I will also plan to address https://github.com/e-mission/e-mission-docs/issues/678 and add silent push notifications to android as well to address https://github.com/e-mission/e-mission-docs/issues/677

shankari commented 2 years ago

Re-checking the permissions.. It looks like we need to get the foreground and background location permissions separately. https://developer.android.com/about/versions/11/privacy/location#request-background-location-separately

We seem to currently be getting them both at the same time, and we are targeting API 30 in this beta version. https://github.com/e-mission/e-mission-data-collection/blob/58ae59add95097dbfdd1f1a9ef2bbcd8f6ec711b/src/android/verification/SensorControlForegroundDelegate.java#L87 Why didn't we run into this?

shankari commented 2 years ago

Checking against the CEO ebike branch since it still has the old UI...

The targetSDK version is 30 (Android 11)

<?xml version='1.0' encoding='utf-8'?>
<manifest android:hardwareAccelerated="true" android:versionCode="16" android:versionName="1.2.1" package="gov.colorado.energyoffice.emission" xmlns:android="http://schemas.android.com/apk/res/android">
  <uses-sdk
    android:minSdkVersion="22"
    android:targetSdkVersion="30" />

we call both permissions

2021-10-27 15:41:54.544 21277-21501/gov.colorado.energyoffice.emission I/SensorPermissionsAndSettingsForegroundDelegate: Both permissions missing, requesting both

        if(!cordova.hasPermission(SensorControlConstants.LOCATION_PERMISSION) &&
          (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) &&
          !cordova.hasPermission(SensorControlConstants.BACKGROUND_LOC_PERMISSION)) {
          Log.i(cordova.getActivity(), TAG, "Both permissions missing, requesting both");
          cordova.requestPermissions(plugin, SensorControlConstants.ENABLE_BOTH_PERMISSION,
            new String[]{SensorControlConstants.LOCATION_PERMISSION, SensorControlConstants.BACKGROUND_LOC_PERMISSION});
          return;
        }

And we get both permissions.

2021-10-27 15:41:55.962 21277-21277/gov.colorado.energyoffice.emission I/SensorPermissionsAndSettingsForegroundDelegate: onRequestPermissionResult called with 362253740
2021-10-27 15:41:55.976 21277-21277/gov.colorado.energyoffice.emission I/SensorPermissionsAndSettingsForegroundDelegate: permissions are [android.permission.ACCESS_FINE_LOCATION, android.permission.ACCESS_BACKGROUND_LOCATION]
2021-10-27 15:41:55.992 21277-21277/gov.colorado.energyoffice.emission I/SensorPermissionsAndSettingsForegroundDelegate: grantResults are [0, 0]
2021-10-27 15:41:56.509 21277-21277/gov.colorado.energyoffice.emission D/SensorControlBackgroundChecker: checkSelfPermission returned 0
2021-10-27 15:41:56.521 21277-21277/gov.colorado.energyoffice.emission D/SensorControlBackgroundChecker: checkLocationPermissions returned true, checking background permission
2021-10-27 15:41:56.532 21277-21277/gov.colorado.energyoffice.emission D/SensorControlBackgroundChecker: checkSelfPermission returned 0
2021-10-27 15:41:56.542 21277-21277/gov.colorado.energyoffice.emission D/SensorControlBackgroundChecker: checkBackgroundLocPermissions returned true, checking location settings

Although cordova just passes the code through to the activity anyway with all the permissions requested at the same time. https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-953383000

shankari commented 2 years ago

Double checked the targetSDK version by using

          Log.i(cordova.getActivity(), TAG,
            "Target SDK is "+cordova.getActivity().getApplicationContext().getApplicationInfo().targetSdkVersion);

We are at target API 30, and it still works. Maybe this only applies for phones running API 30 and above. But at that point, we launch the app settings page anyway, so not sure that it matters.

2021-10-27 16:39:15.730 22678-22934/gov.colorado.energyoffice.emission I/SensorPermissionsAndSettingsForegroundDelegate: Target SDK is 30
2021-10-27 16:39:16.357 22678-22934/gov.colorado.energyoffice.emission I/SensorPermissionsAndSettingsForegroundDelegate: Both permissions missing, requesting both
2021-10-27 16:39:36.776 22678-22934/gov.colorado.energyoffice.emission 2021-10-27 16:39:53.194 22678-22678/gov.colorado.energyoffice.emission I/SensorPermissionsAndSettingsForegroundDelegate: onRequestPermissionResult called with 362253740
2021-10-27 16:39:53.209 22678-22678/gov.colorado.energyoffice.emission I/SensorPermissionsAndSettingsForegroundDelegate: permissions are [android.permission.ACCESS_FINE_LOCATION, android.permission.ACCESS_BACKGROUND_LOCATION]
2021-10-27 16:39:53.226 22678-22678/gov.colorado.energyoffice.emission I/SensorPermissionsAndSettingsForegroundDelegate: grantResults are [0, 0]
2021-10-27 16:39:53.883 22678-22678/gov.colorado.energyoffice.emission D/SensorControlBackgroundChecker: checkSelfPermission returned 0
2021-10-27 16:39:53.922 22678-22678/gov.colorado.energyoffice.emission D/SensorControlBackgroundChecker: checkLocationPermissions returned true, checking background permission
2021-10-27 16:39:53.938 22678-22678/gov.colorado.energyoffice.emission D/SensorControlBackgroundChecker: checkSelfPermission returned 0
2021-10-27 16:39:53.952 22678-22678/gov.colorado.energyoffice.emission D/SensorControlBackgroundChecker: checkBackgroundLocPermissions returned true, checking location settings
shankari commented 2 years ago
  1. So should we split up the location permission requests into two stages or keep it as one stage?
    • As discussed, this only really applies to API 29. For API 28 and below, there are no separate background and foreground permissions.
    • For API 30+, we launch the app settings screen and ask the user to set the permission there directly instead of calling request permissions A: I vote to keep this unchanged for now since we really do need background permission and can't function without it.
  2. Also, should we add the COARSE_LOCATION permission as well? (https://developer.android.com/training/location/permissions#approximate-request)
    • That seems like it is an API 12 requirement so let us table it for now.
shankari commented 2 years ago

It is a bit tricky to figure how how best to merge the background and foreground calls in this case.

The background calls generate different notifications depending on which set of permissions was missing, and clicking the notifications causes different permissions to be requested.

But for the plugin calls, we want to just figure out whether or not the permissions have been granted, at least for check. And "fix" is completely different.

If we could unify the background calls to generate the same notification, we could unify the calls. But then we would not know which permissions are missing. But we might go to the model in which all the background issues basically just go to the status screen. In that case, the unification would be possible. Let's wait and see how the code evolves.

PatGendre commented 2 years ago

Hi @shankari . Ok good Luck on openpath. Non we don't use openid currently.

Le mer. 27 oct. 2021 à 22:27, shankari @.***> a écrit :

@PatGendre https://github.com/PatGendre that was with upgrading everything other than the OpenPATH plugins. I am now working on updating the OpenPATH plugins.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-953283995, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABGOKV3R7GWZR477Y6YO5P3UJBOEFANCNFSM5GEDGAAQ . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.

shankari commented 2 years ago

Moving on to notification checks... This has a pretty simple check: https://developer.android.com/reference/android/app/NotificationManager#areNotificationsEnabled()

"Returns whether notifications from the calling package are blocked."

There is also a similar check: https://developer.android.com/reference/android/app/NotificationManager#areNotificationsPaused()

The documentation for this is: "Returns whether notifications from this package are temporarily hidden. This could be done because the package was marked as distracting to the user via PackageManager#setDistractingPackageRestrictions(String[], int) or because the package is PackageManager#setPackagesSuspended(String[], boolean, PersistableBundle, PersistableBundle, SuspendDialogInfo) suspended."

However, I can't find setDistractingPackageRestrictions or setPackagesSuspended in the PackageManager

Screen Shot 2021-10-28 at 10 25 13 AM
shankari commented 2 years ago

There is a https://developer.android.com/reference/android/content/pm/PackageManager#isPackageSuspended().

Not sure how apps are suspended and how they should be unsuspended. Does this just mean the app lifecycle? Not sure. The only documentation I can find is about apps suspended from the play store, and this note in the release notes. https://developer.android.com/about/versions/pie/android-9.0-changes-28#suspended-apps

wrt non distraction, I do find https://developer.android.com/about/versions/10/behavior-changes-all?hl=de#app-usage

Per-app distraction state - Android 10 can selectively set apps to a "distraction state" where their notifications are suppressed and they do not appear as suggested apps.

The only match that I can find is related to avoiding distracted driving https://source.android.com/devices/automotive/driver_distraction/guidelines

shankari commented 2 years ago

@PatGendre @jf87 @asiripanich @ericafenyo I am thinking of dropping support for android 5.0 It is the only version that still supports static permissions, so dropping it will simplify the codebase somewhat. By targeting API 23 instead of 22, we go from 92% support to 85% support, which doesn't seem like a lot. Thoughts?

(screenshot from Android Studio)

Screen Shot 2021-10-28 at 2 21 39 PM
shankari commented 2 years ago

Suspension does seem to be related to Google Play store suspension. I cannot find any other examples of it, and the the API signature PackageManager#setPackagesSuspended(String[], boolean, PersistableBundle, PersistableBundle, setPackagesSuspended) seems to indicate that the user has other ways of knowing that the app has been suspended.

Finding the appropriate method in the Android Open Source codebase: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/content/pm/PackageManager.java#7912

     * Puts the package in a suspended state, where attempts at starting activities are denied.
     * When the user tries to launch a suspended app, a system dialog with the given dialogMessage

It does not look like the user can fix this. The user can't even launch the activity to fix this.

The distracting packages are different.

* These packages can have certain restrictions set that should discourage the user to launch
them often. For example, notifications from such an app can be hidden, or the app can be removed from launcher suggestions, so the user is able to restrict their use of these apps.

Again, it is not clear how the user can clear these restrictions.

Ah I found it by searching the settings. It is part of the new "Digital Wellness" tools.

shankari commented 2 years ago

There does not seem to be a way to open that screen - there is no settings constant for now. https://developer.android.com/reference/android/provider/Settings

I think we should ignore the notification paused checks for now.

Given all this, I don't think we should add a section on "notifications paused" to the status screen. I am going to commit the change and then roll back the UI only changes. This allows us to re-enable the check with UI-only changes later if we need to.

shankari commented 2 years ago

Also storing the UI change as a patch here in case we need it later. paused_notification_check_support.patch.gz

shankari commented 2 years ago

Moving on to the background restrictions, we start with the auto-reset of permissions for rarely used apps (https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-945047105).

There is a workaround which involves compat libraries from androidx (https://developer.android.com/topic/performance/app-hibernation#handle-hibernation-android10-lower).

However, the API is in the most recent version of androidX, which has a minCompileSDK of 31. cordova supports 30 by default, and doesn't have a way to override without also overridding the target SDK.

Filed https://github.com/apache/cordova-android/issues/1373

Trying to manually override to API 31 and see if it works.

shankari commented 2 years ago

Did not work, got error

> Task :app:compileDebugJavaWithJavac FAILED
An exception has occurred in the compiler (1.8.0_252). Please file a bug against the Java compiler via the Java bug reporting page (http://bugreport.java.com) after checking the Bug Database (http://bugs.java.com) for duplicates. Include your program and the following diagnostic in your report. Thank you.
java.lang.AssertionError: annotationType(): unrecognized Attribute name MODULE (class com.sun.tools.javac.util.UnsharedNameTable$NameImpl)

Downgraded to API 30, confirmed that it worked.

Removed all SDK, just in case that was the issue (to be consistent with https://github.com/invertase/react-native-firebase/issues/5685)

Re-ran with API 30.

Got logs

Checking the license for package Android SDK Platform 30 in /Users/kshankar/Library/Android/sdk/licenses
License for package Android SDK Platform 30 accepted.
Preparing "Install Android SDK Platform 30 (revision: 3)".
"Install Android SDK Platform 30 (revision: 3)" ready.
Installing Android SDK Platform 30 in /Users/kshankar/Library/Android/sdk/platforms/android-30
"Install Android SDK Platform 30 (revision: 3)" complete.
"Install Android SDK Platform 30 (revision: 3)" finished.

There is one SDK

$ ls ~/Library/Android/sdk/platforms/
android-30
shankari commented 2 years ago

Upgraded to compileSdk 31. new SDK was downloaded but got the same error

Checking the license for package Android SDK Platform 31 in /Users/kshankar/Library/Android/sdk/licenses
License for package Android SDK Platform 31 accepted.
Preparing "Install Android SDK Platform 31 (revision: 1)".
"Install Android SDK Platform 31 (revision: 1)" ready.
Installing Android SDK Platform 31 in /Users/kshankar/Library/Android/sdk/platforms/android-31
"Install Android SDK Platform 31 (revision: 1)" complete.
"Install Android SDK Platform 31 (revision: 1)" finished.

An exception has occurred in the compiler (1.8.0_252). Please file a bug against the Java compiler via the Java bug reporting page (http://bugreport.java.com) after checking the Bug Database (http://bugs.java.com) for duplicates. Include your program and the following diagnostic in your report. Thank you.
java.lang.AssertionError: annotationType(): unrecognized Attribute name MODULE (class com.sun.tools.javac.util.UnsharedNameTable$NameImpl)
shankari commented 2 years ago

Upgrading to Java 11 as well compiles and runs. I had to rename Android Studio to Android_Studio to deal with path issues, but once I did that everything worked.

$ echo $JAVA_HOME
/Applications/Android_Studio.app/Contents/jre/Contents/Home

$ $JAVA_HOME/bin/java --version
openjdk 11.0.10 2021-01-19
OpenJDK Runtime Environment (build 11.0.10+0-b96-7281165)
OpenJDK 64-Bit Server VM (build 11.0.10+0-b96-7281165, mixed mode)

$ npx cordova build android
> Configure project :app
Adding classpath: com.google.gms:google-services:4.3.3
WARNING:: Configuration 'compile' is obsolete and has been replaced with 'implementation' and 'api'.
It will be removed in version 7.0 of the Android Gradle plugin.
For more information, see http://d.android.com/r/tools/update-dependency-configurations.html.
WARNING:: Using flatDir should be avoided because it doesn't support any meta-data formats.

> Task :CordovaLib:compileDebugJavaWithJavac
Note: Some input files use or override a deprecated API.
Note: Recompile with -Xlint:deprecation for details.

> Task :app:compileDebugJavaWithJavac
Note: Some input files use or override a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
Note: Some input files use unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

BUILD SUCCESSFUL in 22s
49 actionable tasks: 8 executed, 41 up-to-date
Built the following apk(s):
    .../platforms/android/app/build/outputs/apk/debug/app-debug.apk
shankari commented 2 years ago

If we go down this route, we will need to use java 11 in the CI. On my laptop, I use the built-in java in Android Studio to get Java 11. However, it looks like the Github virtual environments only install the android SDK, not android studio https://github.com/actions/virtual-environments/blob/macOS-11/20211018.1/images/macos/macos-11-Readme.md

We would need to have separate setup scripts for CI and laptop that will set the JAVA_HOME differently. This sounds like a Bad Idea. Downloading JDK 11 from adoptjdk instead...

asiripanich commented 2 years ago

from

Seems reasonable to me to drop support for phones with an Android version released 7 years ago. ;)

PatGendre commented 2 years ago

@PatGendre @jf87 @asiripanich @ericafenyo I am thinking of dropping support for android 5.0 It is the only version that still supports static permissions, so dropping it will simplify the codebase somewhat. By targeting API 23 instead of 22, we go from 92% support to 85% support, which doesn't seem like a lot. Thoughts?

@shankari yes it is reasonable to drop support for Android 5 if it makes thé upgrade easier

shankari commented 2 years ago

Implemented the new checks for the permissions auto-reset. Works fine on both android 30 (apps restricted, need to unrestrict), and android 29 (FEATURE_NOT_AVAILABLE, apps unrestricted)

API OpenPATH Settings
30 Screenshot_1635813608 Screenshot_1635813654
29 Screenshot_1635813033 Screenshot_1635813025
shankari commented 2 years ago

Moving on to the final two tasks for android:

shankari commented 2 years ago

For the first one, there are at least two libraries that attempt to automate the process of launching the screen to fix the background settings.

  1. https://github.com/judemanutd/AutoStarter
  2. https://github.com/WanghongLin/CRomAppWhitelist

(1) supports more devices and has more forks and stars, but it appears to focus only on "AutoStart" settings. I have not seen any issues with push notifications, even on the Huawei from #677, the push notifications are always displayed. What we do have issues with is background optimizations. But the settings screens for both seem to be the same - e.g. From https://github.com/WanghongLin/CRomAppWhitelist/blob/master/cromappwhitelist/src/main/assets/default_appwhitelist.json, the huawei background optimization setting is "com.huawei.systemmanager.optimize.process.ProtectActivity", which is on the backup list for Huawei

Also, (2) only supports Chinese phones, so it doesn't support Samsung, which is the most common phone model that we have had issues with. In fact, it is the only model that we used to warn people about before. https://github.com/e-mission/e-mission-phone/blob/master/www/js/intro.js#L52

      if($window.device.manufacturer.toLowerCase() == "samsung") {
        $scope.backgroundRestricted = true;
        $scope.allowBackgroundInstructions = $translate.instant("intro.allow_background.samsung");
      }

So we have to go with the first option, or something similar. But the first option also has the problem that "com.huawei.systemmanager.optimize.process.ProtectActivity" is on the backup list for Huawei. So it may not be displayed if the first activity is available. But the first activity (StartupNormalAppListActivity) is not on the list for (2), even for autostart. And the activity in (2) for autostart (BootStartActivity) is not in the list for (1).

ARRGGHHH!

So it looks like we may want to try both libraries.

I also note that these may be more restrictive than we need. For example, the OnePlus is listed in (1), but we have reports from two OnePlus users that they don't have any issues. https://github.com/e-mission/e-mission-docs/issues/412#issue-453914743

So making users change settings unnecessarily during onboarding (or later!!) seems like a bad idea.

An alternate approach would be to use stats such as "number of periodic syncs" or "time delta during trip" to determine whether the app is working properly in the background. If it is not, then we ask the user to try various settings.

It would be great if we could record the finally selected settings, but in the absence of callbacks, I don't see an automated way to do that. Not sure if we can ask users to manually record that. We can certainly record whether or not changes were needed for a particular manufacturer/OS combination.

So this argues for the following design:

shankari commented 2 years ago

This seems super complicated, and potentially requires a new POST API or a new user_input or ??? Let's implement the second task on android and finish the implementation on iOS before tackling the first task.

shankari commented 2 years ago

One other issue on android: what if the user denies the permission?

I noticed this behavior while testing the motion activity permission on API level 30.

https://developer.android.com/training/permissions/requesting

At the same time, your app should respect the user's decision to deny a permission. Starting in Android 11 (API level 30), if the user taps Deny for a specific permission more than once during your app's lifetime of installation on a device, the user doesn't see the system permissions dialog if your app requests that permission again. The user's action implies "don't ask again." On previous versions, users would see the system permissions dialog each time your app requested a permission, unless the user had previously selected a "don't ask again" checkbox or option.

https://user-images.githubusercontent.com/2423263/139960862-1dd2cf0b-f9e5-4e20-91d5-b52f66979700.mp4

Of course, even in previous versions, the user could select "Deny and don't ask again". We need to handle all of those. Let's do that first.

https://user-images.githubusercontent.com/2423263/139961392-acd6e247-c0b9-4da9-8013-62cf82712dff.mp4

shankari commented 2 years ago

Alas, it is non-trivial to fix this. The callback in both cases does not distinguish between "denied by user" and "denied automatically by system". In other words, when we call requestPermissions, we don't actually know whether the user will get a prompt to fix the situation or not. And if the permission is denied without showing them the prompt, they will not know what to do.

Let's explore the use of shouldShowRequestPermissionRationale (https://developer.android.com/reference/androidx/core/app/ActivityCompat#shouldShowRequestPermissionRationale(android.app.Activity,%20java.lang.String)) which is underdocumented.

shankari commented 2 years ago

Argh! This API is just bonkers. Apparently shouldShowRequestPermissionRationale:

Verified on android 10 (5 Deny and then "Deny and don't ask again")

2021-11-02 16:20:36.644 24234-24473/edu.berkeley.eecs.emission I/SensorPermissionsAndSettingsForegroundDelegate: shouldShowRequestPermissionRationale = false
2021-11-02 16:20:39.908 24234-24473/edu.berkeley.eecs.emission I/SensorPermissionsAndSettingsForegroundDelegate: shouldShowRequestPermissionRationale = true
2021-11-02 16:20:43.413 24234-24473/edu.berkeley.eecs.emission I/SensorPermissionsAndSettingsForegroundDelegate: shouldShowRequestPermissionRationale = true
2021-11-02 16:20:46.417 24234-24473/edu.berkeley.eecs.emission I/SensorPermissionsAndSettingsForegroundDelegate: shouldShowRequestPermissionRationale = true
2021-11-02 16:20:49.576 24234-24473/edu.berkeley.eecs.emission I/SensorPermissionsAndSettingsForegroundDelegate: shouldShowRequestPermissionRationale = true
2021-11-02 16:20:52.833 24234-24473/edu.berkeley.eecs.emission I/SensorPermissionsAndSettingsForegroundDelegate: shouldShowRequestPermissionRationale = false

This seems to be true on android 11 as well (results of using "fix" 4 times are below) - it is just that don't ask again is the default.

2021-11-02 16:17:40.803 11677-11855/edu.berkeley.eecs.emission I/SensorPermissionsAndSettingsForegroundDelegate: shouldShowRequestPermissionRationale = false
2021-11-02 16:17:43.826 11677-11855/edu.berkeley.eecs.emission I/SensorPermissionsAndSettingsForegroundDelegate: shouldShowRequestPermissionRationale = true
2021-11-02 16:17:46.458 11677-11855/edu.berkeley.eecs.emission I/SensorPermissionsAndSettingsForegroundDelegate: shouldShowRequestPermissionRationale = false
2021-11-02 16:17:49.174 11677-11855/edu.berkeley.eecs.emission I/SensorPermissionsAndSettingsForegroundDelegate: shouldShowRequestPermissionRationale = false

The workaround on SO seems to be to store whether we have asked for this permission earlier or not!!

https://stackoverflow.com/questions/32347532/android-m-permissions-confused-on-the-usage-of-shouldshowrequestpermissionrati

or (accepted) to check the shouldShowRequestPermissionRationale in the callback.

https://stackoverflow.com/a/34612503/4040267

shankari commented 2 years ago

Not quite sure how the flow will work for us in the second case, though given that our UI is not completely native.

Steps are (concretely):

Does it matter? Should we even show an error popup? If we skipped the error popup every time, this would not be a problem. But if there was an error, users like to know. Maybe we can store the value before the call and after the call and determine whether there was a popup or not!!

So we can return PopupShown and PopupWillBeShown from the method.

shankari commented 2 years ago

Another option might be to just always open the app permission settings. That would simplify the user interface. But it would also have a clunkier user interface for the case in which we could have the user accept the permission directly from the status screen.

shankari commented 2 years ago

Now, finally moving on to refactoring the background notification code. We can now simplify that codebase significantly. We currently have increasingly elaborate checks in the background checker that duplicate the foreground check code, but they are increasingly not resolvable automatically and require the app settings screen to be opened. Instead of complicating the logic further, now that we have done all the work of creating the app settings screen, we should just show a generic notification and then open the app settings screen when the user clicks on it.

The big question is, how do we get the appropriate javascript callback and how do we use it to automatically go to the app settings screen?

The scaffolding changes are fairly straightforward:

We already have a set of callbacks in www/js/splash/localnotify.js which allows us to redirect on notification click. When the user clicks on the "app settings incorrect" local notification, we can redirect to the new page.

The trickiest part is ensuring that clicking on the location notification invokes the javascript callback.

The naive approach of clicking on the currently generated notification just triggered the callback in the data collection plugin, but not in javascript.

After spending multiple hours digging through the innards of the local notification plugin, I realized why. It is because the javascript callbacks are triggered only for notifications created through the local notification plugin. This callback is triggered from ClickReceiver.onClick,

    /**
     * Called when local notification was clicked by the user.
     *
     * @param notification Wrapper around the local notification.
     * @param bundle       The bundled extras.
     */
    @Override
    public void onClick(Notification notification, Bundle bundle) {
        String action   = getAction();
        JSONObject data = new JSONObject();

        setTextInput(action, data);
        launchAppIf();

        fireEvent(action, notification, data);

        if (notification.getOptions().isSticky())
            return;

        if (isLast()) {
            notification.cancel();
        } else {
            notification.clear();
        }
    }

The confusing part is that, although ClickReceiver is called a Receiver, it is actually an activity

public class ClickReceiver extends AbstractClickReceiver {

abstract public class AbstractClickReceiver extends Activity {

            <activity
                android:name="de.appplant.cordova.plugin.localnotification.ClickReceiver"
                android:launchMode="singleInstance"
                android:theme="@android:style/Theme.Translucent"
                android:exported="false" />

And is in turn invoked because the pending intent for the notification starts the ClickActivity instead of the main activity for the app.

        Intent intent = new Intent(context, clickActivity)
                .putExtra(Notification.EXTRA_ID, options.getId())
                .putExtra(Action.EXTRA_ID, action.getId())
                .putExtra(Options.EXTRA_LAUNCH, action.isLaunchingApp())
                .setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);

Our existing background notifications launch the main activity of the app, so they don't trigger the local notification callbacks.

    public static void generateLocationEnableNotification(Context ctxt) {
            Intent activityIntent = new Intent(ctxt, MainActivity.class);
            activityIntent.setAction(SensorControlConstants.ENABLE_LOCATION_PERMISSION_ACTION);
            PendingIntent pi = PendingIntent.getActivity(ctxt, SensorControlConstants.ENABLE_LOCATION_PERMISSION,
                    activityIntent, PendingIntent.FLAG_UPDATE_CURRENT);
            NotificationHelper.createNotification(ctxt, SensorControlConstants.ENABLE_LOCATION_PERMISSION,
                    ctxt.getString(R.string.location_permission_off_enable), 
                    pi);
    }

There are two potential high level solutions:

  1. generate local notifications using the LocalNotification plugin, OR
  2. reimplement the javascript fireEvent code in our plugin

While we may want to go with (2) over the long-term if we stop supporting Apache Cordova, it is clearly a silly solution for the short-term.

Given that we are going to go with (1), we still have two options:

  1. we can generate the plugin-compatible local notification directly from the background checker code, OR
  2. we can generate a regular local notification from the background checker code, and convert it to a plugin-compatible local notification from the Foreground delegate.

I think we should go with (2) in this case, because the Foreground Delegate is strongly enmeshed with the cordova framework and callbacks, but the background checker is not. This separation makes it easier to adapt to other UI mechanisms in the future.

shankari commented 2 years ago

we should also consider how to generate the local notification.

  1. What we really want to do is to call the schedule method in the plugin, but unfortunately, that is private.
  2. We considered calling LocalNotify.execute with the schedule parameters but it requires a cordovaCallback to work and since we click on the notification outside the cordova context, cordovaCallback = null in the when the click in processed in onNewIntent.
Screen Shot 2021-11-15 at 11 34 45 PM

The next step is to call it directly using the notification manager in the local notification code. This is what we currently do in the transition notify code. https://github.com/e-mission/e-mission-transition-notify/blob/4f32000935fe73f94d60ef68281eb2f3a343083a/src/android/TransitionNotificationReceiver.java#L172

TODO: Pull out the common dispatch functionality to NotificationHelper or similar so we can generate local notifications from anywhere in our codebase.

shankari commented 2 years ago

Pulled out the common dispatch functionality, but haven't yet converted the TripEnd notification code to use the new functionality, since I don't have an easy way to test it right now. We also don't know whether we want to retain the trip end notification code since it has not been very widely used so far.

Filed issue to clean it up https://github.com/e-mission/e-mission-transition-notify/issues/32

shankari commented 2 years ago

Changes for background sensing done. In addition to the data collection plugin, also needed changes to: https://github.com/e-mission/cordova-unified-logger/pull/35 and https://github.com/e-mission/cordova-usercache/pull/43

Leaving them open for now while we see if there are any other changes required before the merge.

shankari commented 2 years ago

Integrated the app status (at least in basic fashion) into the control screen (https://github.com/e-mission/e-mission-phone/pull/804/commits/539a0fdc9df8af2b436dd23a852364bf50d0c1b1) Now if I can just have the modal popup automatically when the user clicks on the notification, this is demo-worthy.

shankari commented 2 years ago

Tried creating a new state with the same controller and view

  .state('root.main.control.appstatus', {
    url: '/control/appstatus',
    views: {
      'main-control': {
        templateUrl: 'templates/control/main-control.html',
        controller: 'ControlCtrl'
      }
    }
  })

and then redirecting to it. But this naive approach doesn't work since

ionic.bundle.js:26794 Error: Could not resolve 'root.main.control.appstatus ' from state 'root.main.control'
    at Object.transitionTo (ionic.bundle.js:52013)
    at Object.go (ionic.bundle.js:51946)
    at Object.localNotify.handleLaunch (localnotify.js:28)
    at localnotify.js:52
    at processQueue (ionic.bundle.js:29127)
    at ionic.bundle.js:29143
    at Scope.$eval (ionic.bundle.js:30395)
    at Scope.$digest (ionic.bundle.js:30211)
    at ChildScope.$apply (ionic.bundle.js:30503)
    at HTMLButtonElement.<anonymous> (ionic.bundle.js:65426)

switching back to a single state, but with the params defined outside the URL https://github.com/angular-ui/ui-router/wiki/URL-Routing#using-parameters-without-specifying-them-in-state-urls

  .state('root.main.control', {
    url: '/control',
    params: {
        launchAppStatusModal: false
    },
    views: {
      'main-control': {
        templateUrl: 'templates/control/main-control.html',
        controller: 'ControlCtrl'
      }
    }

Bingo!! That worked!!

afterEnter called with stateparams launchAppStatusModal: false
afterEnter called with stateparams launchAppStatusModal: true
shankari commented 2 years ago

Will check in and publish tomorrow and then try to debug the case of the app status icon in the profile page.

shankari commented 2 years ago

At this point, the code is good enough for demo purposes but there are still some hacks that need to be investigated/fixed:

But it is probably good enough to push out as a beta version and get feedback.

shankari commented 2 years ago

One more issue: The hook to rewrite the package name for R does not seem to work for the new file /Users/kshankar/e-mission/e-mission-phone/plugins/cordova-plugin-em-datacollection/src/android/verification/SensorControlChecks.java

walk callback with files = ..../Users/kshankar/e-mission/e-mission-phone/plugins/cordova-plugin-em-datacollection/src/android/verification/SensorControlChecks.java...

file SensorControlBackgroundChecker.java needs to be rewritten, checking package
Handling package: edu.berkeley.eecs.emission.cordova.tracker.verification
sourceFile: /Users/kshankar/e-mission/e-mission-phone/platforms/android/app/src/main/java/edu/berkeley/eecs/emission/cordova/tracker/verification/SensorControlBackgroundChecker.java
file SensorControlForegroundDelegate.java needs to be rewritten, checking package
Handling package: edu.berkeley.eecs.emission.cordova.tracker.verification
sourceFile: /Users/kshankar/e-mission/e-mission-phone/platforms/android/app/src/main/java/edu/berkeley/eecs/emission/cordova/tracker/verification/SensorControlForegroundDelegate.java

Manually fixing and moving on for now..

We should also add a reconfigured build to the CI to catch this in the future.

shankari commented 2 years ago

Another huge issue is that we appear to get a tracking_error generated at app launch. This generates a weird error message and a message saying that we need to click on it to resolve!! And the message is never removed, even after the user fixed everything and consents.

Tried multiple times to figure out where we are getting the tracking error from, but no luck in the time available. Hacking around this for now by making changes only to markConsented but need to figure out the whole init workflow and streamline it.

shankari commented 2 years ago

Added hack

Hack details ``` diff --git a/src/android/DataCollectionPlugin.java b/src/android/DataCollectionPlugin.java index 771e4f9..ed40ca1 100644 --- a/src/android/DataCollectionPlugin.java +++ b/src/android/DataCollectionPlugin.java @@ -1,7 +1,10 @@ package edu.berkeley.eecs.emission.cordova.tracker; +import de.appplant.cordova.plugin.notification.Manager; import edu.berkeley.eecs.emission.R; + + import org.apache.cordova.CordovaPlugin; import org.apache.cordova.CordovaInterface; import org.apache.cordova.CallbackContext; @@ -64,12 +67,14 @@ public class DataCollectionPlugin extends CordovaPlugin { JSONObject newConsent = data.getJSONObject(0); ConsentConfig cfg = new Gson().fromJson(newConsent.toString(), ConsentConfig.class); ConfigManager.setConsented(ctxt, cfg); - TripDiaryStateMachineForegroundService.startProperly(cordova.getActivity().getApplication()); + Manager nMgr = Manager.getInstance(cordova.getActivity()); + nMgr.cancelAll(); // Now, really initialize the state machine // Note that we don't call initOnUpgrade so that we can handle the case where the // user deleted the consent and re-consented, but didn't upgrade the app // mControlDelegate.checkAndPromptPermissions(); - // ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_initialize)); + ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_initialize)); + TripDiaryStateMachineForegroundService.startProperly(cordova.getActivity().getApplication()); // TripDiaryStateMachineReceiver.restartCollection(ctxt); callbackContext.success(); return true; ```

With the hack:

This is bizarre and freaky but I'm not going to look a gift horse in the mouth now. Tested on android 8 as well; starting the upgrade process now...

shankari commented 2 years ago

Feedback from Sandee:

shankari commented 2 years ago

After many months, and fixing a couple of side issues (https://github.com/e-mission/e-mission-phone/pull/804/commits/4378a70d9638f0e2bf6647c180d95e3061dcbbe4, https://github.com/e-mission/e-mission-phone/pull/805, https://github.com/e-mission/e-mission-phone/pull/808, https://github.com/e-mission/e-mission-phone/pull/809) let's return here and:

shankari commented 2 years ago

Starting with https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-971825731, the hook searches for

const R_RE = /[^\.\w]R\./;
...
                        if (contents.match(R_RE) || contents.match(BUILDCONFIG_RE) || contents.match(MAINACT_RE)) {
                            console.log('file '+filename+' needs to be rewritten, checking package');

but the R package is actually unused in SensorControlChecks.java. So there is no R. and the match fails. The solution is to remove the unused import.

shankari commented 2 years ago

wrt

Will check in and publish tomorrow and then try to debug the case of the app status icon in the profile page.

not sure what the problem with the app status icon on the profile page is.

Ah I think it is related to:

overallAppStatus on the profile page is not getting updated for this to work

when it should be updated from

          <permissioncheck overallstatus="overallAppStatus"></permissioncheck>

I don't think that it a super high priority right now since users can also use the button to check app status, not only fix it.

shankari commented 2 years ago

I looked into it a little more, and we do in fact compute the overallAppStatus in the app status directive controller. However, because it is not correctly linked to theoverallAppStatus` field in the profile scope, nothing gets updated correctly. I'm going to try one more time to fix it, but otherwise, I am moving on.

Note that the overallAppStatus in the directive appears to be correct, so if I change the onClick to ng-click="appStatusChecked(overallAppStatus)" and add a new line in the callback, the status is updated correctly. Need to figure out whether we can return any value during modal initialization.

+        $scope.overallAppStatus = newStatus;
shankari commented 2 years ago

I tried to read the value from the modal once it was created. Unfortunately, the modal is returned before the status is recomputed.

I then added a breakpoint into another function and invoked if after the modal was initialized. The overallAppStatus still was false.

This is going to take too much time, and it is complicated and potentially angular1 specific. I think that the correct solution is to refactor the app status module to be a service and then read the value from the service, but the ROI on that is not great.

Here's the change to restore it in case we need it again.

change_app_status_color_and_icon.patch.gz

shankari commented 2 years ago

seemingly infinite loop between waiting_for_trip_start and tracking_error when the motion activity is turned off

we should definitely fix this and this

Another huge issue is that we appear to get a tracking_error generated at app launch. This generates a weird error message and a message saying that we need to click on it to resolve!! And the message is never removed, even after the user fixed everything and consents.

and then I think we should move on to iOS

shankari commented 2 years ago

We start the periodic sync

2021-07-27 16:12:45.427 22374-22921/edu.berkeley.eecs.emission I/TripDiaryStateMachineRcvr: START PERIODIC ACTIVITY

Foreground service starts

2021-07-27 16:12:49.795 22374-22921/edu.berkeley.eecs.emission D/TripDiaryStateMachineForegroundService: startProperly called with context = edu.berkeley.eecs.emission.MainActivity@b3efb44
2021-07-27 16:12:49.828 22374-22374/edu.berkeley.eecs.emission D/TripDiaryStateMachineForegroundService: onCreate called
2021-07-27 16:12:49.842 22374-22374/edu.berkeley.eecs.emission D/TripDiaryStateMachineForegroundService: onStartCommand called with intent = Intent { cmp=edu.berkeley.eecs.emission/.cordova.tracker.location.TripDiaryStateMachineForegroundService } flags = 0 and startId = 1

We generate an "initialize"

      TripDiaryStateMachineForegroundService.startProperly(ctxt);
      // It is not enough to move to the foreground; you also need to reinitialize tracking
      // https://github.com/e-mission/e-mission-docs/issues/580#issuecomment-700747931
      ctxt.sendBroadcast(new ExplicitIntent(ctxt, R.string.transition_initialize));

And we launch a bunch of checks for the app state

        if (intent == null) {
          SensorControlBackgroundChecker.checkAppState(this);
          message = humanizeState(this, TripDiaryStateMachineService.getState(this));
        }

So far, so good.

The checks indicate that motion activity is turned off and so we generated another notification to fix it.

2021-07-27 16:12:51.637 22374-22921/edu.berkeley.eecs.emission I/SensorControlBackgroundChecker: Curr status check results =  loc permission, motion permission, notification, unused apps [true, false, true, true]
2021-07-27 16:12:51.780 22374-22921/edu.berkeley.eecs.emission D/NotificationHelper: Generating notify with id 362253739, message Click to view and fix app status and pending intent PendingIntent{1278d18: android.os.BinderProxy@3649371}

We receive and handle the initialize, note that it uses a new thread (22374-22374)

2021-07-27 16:13:04.370 22374-22374/edu.berkeley.eecs.emission I/TripDiaryStateMachineRcvr: TripDiaryStateMachineReciever onReceive(android.app.ReceiverRestrictedContext@eaf447e, Intent { act=local.transition.initialize flg=0x10 pkg=edu.berkeley.eecs.emission cmp=edu.berkeley.eecs.emission/.cordova.tracker.location.TripDiaryStateMachineReceiver }) called
2021-07-27 16:13:16.758 22374-22921/edu.berkeley.eecs.emission I/TripDiaryStateMachineRcvr: END PERIODIC ACTIVITY

We generate another start command because we received the initialize

if (intent.getAction().equals(context.getString(R.string.transition_initialize))) {
    ...
    TripDiaryStateMachineForegroundService.startProperly(context);
}
2021-07-27 16:13:19.818 22374-22374/edu.berkeley.eecs.emission D/TripDiaryStateMachineForegroundService: startProperly called with context = android.app.ReceiverRestrictedContext@eaf447e
2021-07-27 16:13:25.027 22374-22374/edu.berkeley.eecs.emission D/TripDiaryStateMachineForegroundService: onStartCommand called with intent = Intent { cmp=edu.berkeley.eecs.emission/.cordova.tracker.location.TripDiaryStateMachineForegroundService } flags = 0 and startId = 2

Step 1: We should consider removing this circular dependency.

Let's revert the hack, see if this is the case and then see how to fix it.

shankari commented 2 years ago

reverted hack and fixed, picking back up on infinite loop

shankari commented 2 years ago

Picking up after the startProperly call, we restart the service

2021-07-27 16:13:25.027 22374-22374/edu.berkeley.eecs.emission D/TripDiaryStateMachineForegroundService: onStartCommand called with intent = Intent { cmp=edu.berkeley.eecs.emission/.cordova.tracker.location.TripDiaryStateMachineForegroundService } flags = 0 and startId = 2

Next, we handle the initialize and begin re-creating the geofence

2021-07-27 16:13:25.272 22374-22374/edu.berkeley.eecs.emission D/TripDiaryStateMachineService: handleAction(local.state.waiting_for_trip_start, local.transition.initialize) called
...
2021-07-27 16:13:25.661 22374-22983/edu.berkeley.eecs.emission D/CreateGeofenceAction: About to start waiting for location

In parallel, we start handling the tracking_error that we generated earlier from the checkAppStatus after re-creating the foreground service.

2021-07-27 16:13:56.783 22374-22374/edu.berkeley.eecs.emission I/TripDiaryStateMachineRcvr: TripDiaryStateMachineReciever onReceive(android.app.ReceiverRestrictedContext@eaf447e, Intent { act=local.transition.tracking_error flg=0x10 pkg=edu.berkeley.eecs.emission cmp=edu.berkeley.eecs.emission/.cordova.tracker.location.TripDiaryStateMachineReceiver }) called

we get back the location and create the geofence

2021-07-27 16:15:37.512 22374-22374/edu.berkeley.eecs.emission D/TripDiaryStateMachineService: newState saved in prefManager is local.state.waiting_for_trip_start

as part of the regular checks after setting the state, we notice that the motion activity is bad, so we set the new state to start

2021-07-27 16:16:39.656 22374-22374/edu.berkeley.eecs.emission I/SensorControlBackgroundChecker: Curr status check results =  loc permission, motion permission, notification, unused apps [true, false, true, true]
2021-07-27 16:16:41.331 22374-22374/edu.berkeley.eecs.emission D/NotificationHelper: Generating notify with id 362253739, message Click to view and fix app status and pending intent PendingIntent{280b2ec: android.os.BinderProxy@5d846b5}
2021-07-27 16:17:30.871 22374-22374/edu.berkeley.eecs.emission D/TripDiaryStateMachineService: newState after handling action is local.state.start

Then we check the settings again

2021-07-27 16:17:34.264 22374-22374/edu.berkeley.eecs.emission I/SensorControlBackgroundChecker: Curr status check results =  loc permission, motion permission, notification, unused apps [true, false, true, true]
2021-07-27 16:17:34.448 22374-22374/edu.berkeley.eecs.emission D/NotificationHelper: Generating notify with id 362253739, message Click to view and fix app status and pending intent PendingIntent{54c7384: android.os.BinderProxy@5d846b5}

But now we are in the start state, so we restart the FSM by generating an initialize

2021-07-27 16:17:34.471 22374-22374/edu.berkeley.eecs.emission I/SensorControlBackgroundChecker: in restartFSMIfStartState, currState = local.state.start
2021-07-27 16:17:34.483 22374-22374/edu.berkeley.eecs.emission I/SensorControlBackgroundChecker: in start state, sending initialize

So the loop is:

The real problem is that we are in two minds about how to treat non location permissions. In the FSM, we treat them as optional, so once the geofence is created, we go to waiting_for_trip_start. In the sensor control checks, we treat them as required, so we generate a tracking error if they are not present.

We need to figure out how we want to represent these. This will involve community input, so I have created https://github.com/e-mission/e-mission-docs/issues/700 to track it. For now, we will treat them as optional, which means that we only need to generate a tracking error if the location is turned off.

shankari commented 2 years ago

Last remaining issues on android: