Code Monkey home page Code Monkey logo

once's Introduction

Once

Android Weekly Android Arsenal Build Status

A small Android library to manage one-off operations for API 14 and higher.


Some things should happen once.

  • Users should only get the guided tour once.
  • Release notes should only pop up once every app upgrade.
  • Your app should only phone home to update content once every hour.

Once provides a simple API to track whether or not your app has already performed an action within a given scope.

Usage

First things first, you'll need to initialise Once on start up. In your Application class's onCreate() override add the follow:

Once.initialise(this);

Checking if something has been done

Now you're ready to go. Say you wanted to navigate to a 'WhatsNew' Activity every time your app is upgraded:

String showWhatsNew = "showWhatsNewTag";

if (!Once.beenDone(Once.THIS_APP_VERSION, showWhatsNew)) {
    startActivity(new Intent(this, WhatsNewActivity.class));
    Once.markDone(showWhatsNew);
}

Or if you want your app tour to be shown only when a user install, simply check the tag using the THIS_APP_INSTALL scope:

if (!Once.beenDone(Once.THIS_APP_INSTALL, showAppTour)) {
    ...
    Once.markDone(showAppTour);
}

Your app operations can also be rate-limited by time spans. So for example if you only want to phone back to your server a maximum of once per hour, you'd do the following:

if (!Once.beenDone(TimeUnit.HOURS, 1, phonedHome) { ... }

If checking by time bounds is not enough you can manually get the Date of last time a tag was marked done by:

Date lastDone = Once.lastDone(brushedTeeth);

This will return null if the tag has never been marked as done.

Marking something as to do

Say one part of your app triggers functionality elsewhere. For example you might have some advanced feature onboarding to show on the main activity, but you only want to show it once the user has seen the basic functionality.

// in the basic functionality activity
Once.toDo(Once.THIS_APP_INSTALL, "show feature onboarding");
...

// back in the home activity
if (Once.needToDo(showAppTour)) {
    // do some operations
    ...

    // after task has been done, mark it as done as normal
    Once.markDone(showAppTour);
}

When a task is marked done it is removed from the set of tasks 'to do' so subsequent needToDo(tag) calls will return false. To stop the tag from being added back to your todo list each time the user looks at the basic functionality task, we've added a scope to the todo call: toDo(Once.THIS_APP_INSTALL, tag). You could also use the THIS_APP_VERSION scope for todo's which should happen once per app version, or leave off scope complete for tasks which should be repeated every time.

Doing something once per X events

Sometimes you need to keep track of how many times something has happened before you act on it. For example, you could prompt the user to rate your app after they've used the core functionality three times.

// Once again in the basic functionality activity
Once.markDone("action");
if (Once.beenDone("action", Amount.exactly(3))) {
    showRateTheAppDialog();
}

You can also change the count checking from exactly(int x) times, to either Amount.lessThan(int x) times or Amount.moreThan(int x) times. When you don't specific a particular amount, Once will default to Amount.moreThan(0) i.e. checking if it's ever been done at all.

To de-noise your code a bit more you can also static-import the Once methods, so usage looks a bit cleaner

import static jonathanfinerty.once.Once.THIS_APP_INSTALL;
import static jonathanfinerty.once.Once.beenDone;
import static jonathanfinerty.once.Once.markDone;

...
...

if (!beenDone(THIS_APP_VERSION, tagName)) {
    ...
    markDone(showWhatsNew);
}

Installation

Add a library dependency to your app module's build.gradle:

dependencies {
    compile 'com.jonathanfinerty.once:once:1.3.1'
}

You'll need to have mavenCentral() in your list of repositories

Example

Try out the sample app here: https://play.google.com/store/apps/details?id=jonathanfinerty.onceexample and have a look at it's source code in once-example/ for more simple usage.

Contributing

Once was made in '20%' time at Huddle, where its used to help build our Android apps. Pete O'Grady and Paul Simmons also provided invaluable feedback.

Pull requests and github issues are more than welcome and you can get in touch with me directly @jonfinerty.

License

Copyright 2021 Jon Finerty

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

once's People

Contributors

donglua avatar guilhermesgb avatar jonfinerty avatar marcocaloiaro1994 avatar medyo avatar piasy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

once's Issues

[Feature] Once.beenDone(scope, tag, times)

Once.beenDone(scope, tag, times) would only return true when that method has been called that specific number of times or more AND the item itself has not been done.

Example 1

Once.beenDone(THIS_APP_INSTALL, "foo", 3); // false
Once.beenDone(THIS_APP_INSTALL, "foo", 3); // false
Once.beenDone(THIS_APP_INSTALL, "foo", 3); // true
Once.beenDone(THIS_APP_INSTALL, "foo", 4); // true

Example 2

Once.beenDone(THIS_APP_INSTALL, "bar", 3); // false
Once.beenDone(THIS_APP_INSTALL, "bar", 3); // false
Once.beenDone(THIS_APP_INSTALL, "bar", 3); // true
Once.markDone("bar");
Once.beenDone(THIS_APP_INSTALL, "bar", 4); // false

What do you think of that?

My use case for that would be something rather annoying for the user and you don't want to show it always. (Ask them for rating the applications, inviting friends etc).

ConcurrentModificationException

Hi! I've seen some crash reports in Crashlytics of

Caused by java.util.ConcurrentModificationException
       at java.util.ArrayList$Itr.next + 860(ArrayList.java:860)
       at jonathanfinerty.once.PersistedMap.listToString + 105(PersistedMap.java:105)
       at jonathanfinerty.once.PersistedMap.put + 79(PersistedMap.java:79)
       at jonathanfinerty.once.Once.markDone + 268(Once.java:268)
...

and

Caused by java.util.ConcurrentModificationException
       at java.util.ArrayList$ArrayListIterator.next + 573(ArrayList.java:573)
       at jonathanfinerty.once.PersistedMap.listToString + 105(PersistedMap.java:105)
       at jonathanfinerty.once.PersistedMap.put + 79(PersistedMap.java:79)
       at jonathanfinerty.once.Once.markDone + 268(Once.java:268)
...

Not sure if there is any limitation regarding thread safety of Once that I'm not aware.
Thank you for the help!

Got crash error on beenDone from users from google play console

I got this error on Crashes & ANRs in google play console from 10 users:

Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'java.util.List ry.a(java.lang.String)' on a null object reference
	at jonathanfinerty.once.Once.initialise(Once.java)
	at <OR>.beenDone(Once.java)
	at <OR>.beenDone(Once.java)
	at <OR>.beenDone(Once.java)
	at <OR>.beenDone(Once.java)
	at <OR>.beenDone(Once.java)
	at <OR>.markDone(Once.java)
	at jonathanfinerty.once.Once.initialise(Once.java)
	at <OR>.beenDone(Once.java)
	at <OR>.beenDone(Once.java)
	at <OR>.beenDone(Once.java)
	at <OR>.beenDone(Once.java)
	at <OR>.beenDone(Once.java)
	at <OR>.markDone(Once.java)

I call Once.initialise(this) on the app class.

and this error raised in a class where I try to use Once.beenDone normally on background thread.

ConcurrentModificationException

Caused by java.util.ConcurrentModificationException
java.util.ArrayList$Itr.next (ArrayList.java:860)
jonathanfinerty.once.Once.beenDone (Once.java:254)
jonathanfinerty.once.Once.beenDone (Once.java:219)
jonathanfinerty.once.Once.beenDone (Once.java:204)
...

"Culprit" is the list inside the map in PersistedMap.java
https://github.com/jonfinerty/Once/blob/master/once/src/main/java/jonathanfinerty/once/PersistedMap.java#L18

Synchronizing access to the list at it is now, could be a little bit of a pain.

Might consider annotating every public static function in Once.java with synchronized. I think this step would at worst cost only a few milliseconds.

Tests

Needs tests to help maintainability and confidence

I think there is a bug

If I uninstall the app or clear data,it will reset all buttons.But I don't want it happen,How can I do?

Upgrade to 1.0.0

I've upgrade from 0.5.0 to 1.0.0

My app crashes on Once.initialise(this); in my Application class. I've included the logcat dump

logcat.txt

Could not find com.jonathanfinerty.once:once:1.2.2

Could not resolve all files for configuration ':app:releaseRuntimeClasspath'.
Could not find com.jonathanfinerty.once:once:1.2.2.
Searched in the following locations:
- https://maven.google.com/com/jonathanfinerty/once/once/1.2.2/once-1.2.2.pom
- https://repo.maven.apache.org/maven2/com/jonathanfinerty/once/once/1.2.2/once-1.2.2.pom
- file:/root/.m2/repository/com/jonathanfinerty/once/once/1.2.2/once-1.2.2.pom
- https://plugins.gradle.org/m2/com/jonathanfinerty/once/once/1.2.2/once-1.2.2.pom
- file:/builds/wilix/clients/mobile/node_modules/react-native/android/com/jonathanfinerty/once/once/1.2.2/once-1.2.2.pom
- file:/builds/wilix/clients/mobile/node_modules/jsc-android/dist/com/jonathanfinerty/once/once/1.2.2/once-1.2.2.pom
- https://dl.google.com/dl/android/maven2/com/jonathanfinerty/once/once/1.2.2/once-1.2.2.pom
- https://jitpack.io/com/jonathanfinerty/once/once/1.2.2/once-1.2.2.pom

Migrate away from JCenter

JFrog has announced that it will be shutting down JCenter on February 2nd 2022. JCenter is the sole repository hosting this library, so it will become unavailable after this date unless migrated to an alternative service.

Once every timespan

Maybe implement functionality to perform actions a maximum of once per a timespan.

e.g don't send analytic's data more that once per hour, or don't do a animation if its been done in the last minute.

constant prefs filename

SharedPreferences in PersistedMap are created with name:

PersistedMap.class.getSimpleName() + mapName

however if class is obfuscated it might have different name on further app versions, so I'm guessing it would be better to protect class from obfuscation or give preferences a constant name (or allow to specify it on client side)

Rename Library

I'm not a huge fan of Saw

Given its usage in the readme, maybe Once, or something related is a better name

Hey, got some strict mode violation warning

D/StrictMode: StrictMode policy violation; ~duration=8 ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=19 violation=2
                                                                       at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1137)
                                                                       at android.app.SharedPreferencesImpl.awaitLoadedLocked(SharedPreferencesImpl.java:202)
                                                                       at android.app.SharedPreferencesImpl.getAll(SharedPreferencesImpl.java:214)
                                                                       at jonathanfinerty.once.PersistedMap.<init>(PersistedMap.java:17)
                                                                       at jonathanfinerty.once.Once.initialise(Once.java:34)
                                                                       at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1034)
                                                                       at android.app.ActivityThread.handleBindApplication(ActivityThread.java:4603)
                                                                       at android.app.ActivityThread.access$1500(ActivityThread.java:148)
                                                                       at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1353)
                                                                       at android.os.Handler.dispatchMessage(Handler.java:102)
                                                                       at android.os.Looper.loop(Looper.java:135)
                                                                       at android.app.ActivityThread.main(ActivityThread.java:5310)
                                                                       at java.lang.reflect.Method.invoke(Native Method)
                                                                       at java.lang.reflect.Method.invoke(Method.java:372)
                                                                       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:901)
                                                                       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:696)`

`D/StrictMode: StrictMode policy violation; ~duration=4 ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=19 violation=2
                                                                       at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1137)
                                                                       at android.app.SharedPreferencesImpl.awaitLoadedLocked(SharedPreferencesImpl.java:202)
                                                                       at android.app.SharedPreferencesImpl.getStringSet(SharedPreferencesImpl.java:230)
                                                                       at jonathanfinerty.once.PersistedSet.<init>(PersistedSet.java:19)
                                                                       at jonathanfinerty.once.Once.initialise(Once.java:38)
                                                                       at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1034)
                                                                       at android.app.ActivityThread.handleBindApplication(ActivityThread.java:4603)
                                                                       at android.app.ActivityThread.access$1500(ActivityThread.java:148)
                                                                       at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1353)
                                                                       at android.os.Handler.dispatchMessage(Handler.java:102)
                                                                       at android.os.Looper.loop(Looper.java:135)
                                                                       at android.app.ActivityThread.main(ActivityThread.java:5310)
                                                                       at java.lang.reflect.Method.invoke(Native Method)
                                                                       at java.lang.reflect.Method.invoke(Method.java:372)
                                                                       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:901)
                                                                       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:696)

Provide a changelog

Please, provide a quick changelog.
It's not clear what changed between version 1.2.2 and version 1.3.0

NumberFormatException: Invalid long

Fatal Exception: java.lang.RuntimeException: Unable to create application com.cyou.cma.clauncher.LauncherApplication: java.lang.NumberFormatException: Invalid long: "149744985210°"
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:4728)
at android.app.ActivityThread.access$1500(ActivityThread.java:151)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1326)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:136)
at android.app.ActivityThread.main(ActivityThread.java:5421)
at java.lang.reflect.Method.invokeNative(Method.java)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:979)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:795)
at dalvik.system.NativeStart.main(NativeStart.java)
Caused by java.lang.NumberFormatException: Invalid long: "149744985210°"
at java.lang.Long.invalidLong(Long.java:124)
at java.lang.Long.parse(Long.java:361)
at java.lang.Long.parseLong(Long.java:352)
at java.lang.Long.parseLong(Long.java:318)
at jonathanfinerty.once.PersistedMap.stringToList(PersistedMap.java:1124)
at jonathanfinerty.once.PersistedMap.get(PersistedMap.java:59)
at jonathanfinerty.once.Once.beenDone(Once.java:166)
at jonathanfinerty.once.Once.beenDone$4f70807c(Once.java:151)
at com.cyou.cma.clauncher.LauncherApplication.onCreate(LauncherApplication.java:282)
at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1019)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:4725)
at android.app.ActivityThread.access$1500(ActivityThread.java:151)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1326)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:136)
at android.app.ActivityThread.main(ActivityThread.java:5421)
at java.lang.reflect.Method.invokeNative(Method.java)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:979)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:795)
at dalvik.system.NativeStart.main(NativeStart.java)

Get list of tasks that have been done

It would be really useful to retrieve a list of tasks that have been marked as done.

Something like:
var doneTasks: List<String> = Once.getAllDone()

crashes on startup in apk but is working on emulator

I used

if (!Once.beenDone(Once.THIS_APP_INSTALL, showAppTour)) { ... Once.markDone(showAppTour); }

and it works fine in release build on the emulator but when I compile it into an apk, it crashes on startup.

Is their a proguard line of code that Once needs?

Using Once right after app starts causes thread to freeze

Using any Once method right when the application starts causes the entire thread to freeze. In my case, I'd like to use this library in my MainActivity to know if I should show the user an onboarding screen. However, calling Once.beenDone("seen_onboarding") caused a blank screen to be shown.

I was able to replicate this effect on a sample app quite easily with the following steps:

  1. Call Once.initialise(this) in the app's Application class onCreate().
  2. Call any Once method inside the MainActivity's onCreate().

Doing this will cause a blank screen. It's pretty straightforward but I can provide the sample app code if needed. This only happens in version 1.0.2 of the library. I'm guessing it's because the asynchronous loading never wakes up the thread that called it before it finished loading.

Get on JCenter

This would remove the need of

repositories {
    maven { url 'https://dl.bintray.com/jonathanfinerty/maven' }
}

when using the library

License

Needs a license so people know whether or not they can use it

This project uses AndroidX dependencies, but the 'android.useAndroidX' property is not enabled

Bug report:

This project uses AndroidX dependencies, but the 'android.useAndroidX' property is not enabled. Set this property to true in the gradle.properties file and retry.
The following AndroidX dependencies are detected: androidx.annotation:annotation:1.2.0

implementation 'androidx.annotation:annotation:1.2.0'

I think you don't need this line.

Gradle version: 6.1.1

Example App

It would be great to have an example app using the library

[Feature Request] Allow us to specify listeners for Once.initialise() completion

I really enjoy using this library but my only issue with it is the time it takes to initialize.

I understand that Once initializes in the background but using it blocks the calling thread until it has finished initializing. In my app, I use Once.beenDone() in the onCreate() method of my first activity therefore the main thread gets blocked during startup. I would prefer it if there was some way to attach listeners to notify us as soon as Once.initialise() has finished running. :)

To Do tasks fail to be persisted when force-stopping app

If you mark a task as "to do" and then force stop the app, this will not be persisted.

This appears to be caused by a misuse of getStringSet() in PersistedSet - the docs state:

Note that you must not modify the set instance returned by this call. The consistency of the stored data is not guaranteed if you do, nor is your ability to modify the instance at all.

However PersistedSet modifies this instance rather than a copy of it, resulting in the lack of persistence when force-stopping (and possibly other bugs). The constructor should instead read the preference into memory like this:

set = new HashSet<String>(preferences.getStringSet(STRING_SET_KEY, new HashSet<String>()));

Installation

Needs instructions + implementation on how to install via gradle

How to use, Once times days, minutes

Great library,
Since I can use for each X days run a process?

I have done with total times the ap runs, it works correctly

final static String APP_RATE_COUNT = "app_rate_count";
final static int APP_RATE_LAUNCHES = 3;

        Once.markDone(APP_RATE_COUNT);
        if (Once.beenDone(APP_RATE_COUNT, Amount.moreThan(APP_RATE_LAUNCHES))) {
            Once.clearDone(APP_RATE_COUNT);
            showDialogAppRate();
        }

But I try to do the equivalent but time units

Javadocs

Once the api is stable it would be nice to provide Javadocs so people can use ctrl-q for quick reference information

More Examples

Add examples of THIS_APP_VERSION and time spans usage to the readme

VERSION vs INSTALL tags backwards?

Maybe my understanding is wrong, but it seems THIS_APP_VERSION and THIS_APP_INSTALL are backwards. The VERSION tag is using package lastUpdateTime... shouldn't it be using package versionCode? Install is date oriented, while version is build number instead.

Question about toDo

Reviewing the public static void toDo(@Scope int scope, String tag) function specification it says "if it has already marked to do or been done within that scope then it will not be marked." but the actual implementation does not involve the THIS_APP_SESSION scope. I'm wondering why the implementation does not check if the tag was done within that scope? Maybe we should have a session start timestamp in order to do that... what do you think?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.