dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
22.26k stars 1.76k forks source link

[Bug] [platform/android] [area-essentials] Shared Preference Android #24397

Open AugPav opened 3 months ago

AugPav commented 3 months ago

Description

Error when trying to read a shared preference of type long

Steps to Reproduce

1 - The shared preference is created.

new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds() is of type long

Preferences.Set("SYNC_MASTER_DATE", new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds());

2 - An attempt is made to read the shared preference.

var SyncMasterLast = Preferences.Get("SYNC_MASTER_DATE", 0)

Error

ex {Java.Lang.ClassCastException: java.lang.Long cannot be cast to java.lang.Integer at Java.Interop.JniEnvironment.InstanceMethods.CallIntMethod(JniObjectReference instance, JniMethodInfo method, JniArgumentValue args) in /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/obj/Release/net7.0/JniEnvironment.g.cs:line 20203 at Android.Runtime.JNIEnv.CallIntMethod(IntPtr jobject, IntPtr jmethod, JValue parms) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNIEnv.g.cs:line 192 at Android.Content.ISharedPreferencesInvoker.GetInt(String key, Int32 defValue) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/obj/Release/net8.0/android-34/mcw/Android.Content.ISharedPreferences.cs:line 830 at Microsoft.Maui.Storage.PreferencesImplementation.Get[Int32](String key, Int32 defaultValue, String sharedName) in //src/Essentials/src/Preferences/Preferences.android.cs:line 110 at Microsoft.Maui.Storage.Preferences.Get(String key, Int32 defaultValue, String sharedName) in //src/Essentials/src/Preferences/Preferences.shared.cs:line 185 at Microsoft.Maui.Storage.Preferences.Get(String key, Int32 defaultValue) in /_/src/Essentials/src/Preferences/Preferences.shared.cs:line 116 at SelfInspectionMaui.Services.SyncMaster.SyncMasterLogica() in D:\DevRd\SelfInspectionMaui\SelfInspectionMaui\Services\SyncMaster.cs:line 96 --- End of managed Java.Lang.ClassCastException stack trace --- java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.Integer at android.app.SharedPreferencesImpl.getInt(SharedPreferencesImpl.java:321) at mono.java.lang.RunnableImplementor.n_run(Native Method) at mono.java.lang.RunnableImplementor.run(RunnableImplementor.java:31) at android.os.Handler.handleCallback(Handler.java:958) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loopOnce(Looper.java:230) at android.os.Looper.loop(Looper.java:319) at android.app.ActivityThread.main(ActivityThread.java:8919) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:578) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1103)

--- End of managed Java.Lang.ClassCastException stack trace --- java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.Integer at android.app.SharedPreferencesImpl.getInt(SharedPreferencesImpl.java:321) at mono.java.lang.RunnableImplementor.n_run(Native Method) at mono.java.lang.RunnableImplementor.run(RunnableImplementor.java:31) at android.os.Handler.handleCallback(Handler.java:958) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loopOnce(Looper.java:230) at android.os.Looper.loop(Looper.java:319) at android.app.ActivityThread.main(ActivityThread.java:8919) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:578) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1103) } Java.Lang.ClassCastException

Link to public reproduction project repository

No response

Version with bug

8.0.80 SR8

Is this a regression from previous behavior?

Not sure, did not test other versions

Last version that worked well

Unknown/Other

Affected platforms

Android

Affected platform versions

Android 14, Android 13

Did you find any workaround?

Why does this error occur?

When trying to read the shared preference, the following code is executed for [Android]

https://github.com/dotnet/maui/blob/cf42c193957a530af1a0551284c40e72e55780f9/src/Essentials/src/Preferences/Preferences.android.cs

Line 1  public T Get<T>(string key, T defaultValue, string sharedName)                                      
Line 2          {                               
Line 3              lock (locker)                           
Line 4              {                           
Line 5                  object value = null;                        
Line 6                  using (var sharedPreferences = GetSharedPreferences(sharedName))                        
Line 7                  {                       
Line 8                      if (defaultValue == null)                   
Line 9                      {                   
Line 10                         value = sharedPreferences.GetString(key, null);             
Line 11                     }                   
Line 12                     else                    
Line 13                     {                   
Line 14                         switch (defaultValue)               
Line 15                         {               
Line 16                             case int i:         
Line 17                                 value = sharedPreferences.GetInt(key, i);       
Line 18                                 break;      
Line 19                             case bool b:            
Line 20                                 value = sharedPreferences.GetBoolean(key, b);       
Line 21                                 break;      
Line 22                             case long l:            
Line 23                                 value = sharedPreferences.GetLong(key, l);      
Line 24                                 break;      
Line 25                             case double d:          
Line 26                                 var savedDouble = sharedPreferences.GetString(key, null);       
Line 27                                 if (string.IsNullOrWhiteSpace(savedDouble))     
Line 28                                 {       
Line 29                                     value = defaultValue;   
Line 30                                 }       
Line 31                                 else        
Line 32                                 {       
Line 33                                     if (!double.TryParse(savedDouble, NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out var outDouble))   
Line 34                                     {   
Line 35                                         var maxString = Convert.ToString(double.MaxValue, CultureInfo.InvariantCulture);
Line 36                                         outDouble = savedDouble.Equals(maxString, StringComparison.Ordinal) ? double.MaxValue : double.MinValue;
Line 37                                     }   
Line 38                                         
Line 39                                     value = outDouble;  
Line 40                                 }       
Line 41                                 break;      
Line 42                             case float f:           
Line 43                                 value = sharedPreferences.GetFloat(key, f);     
Line 44                                 break;      
Line 45                             case string s:          
Line 46                                 // the case when the string is not null     
Line 47                                 value = sharedPreferences.GetString(key, s);        
Line 48                                 break;      
Line 49                             case DateTime dt:           
Line 50                                 var encodedValue = sharedPreferences.GetLong(key, dt.ToBinary());       
Line 51                                 value = DateTime.FromBinary(encodedValue);      
Line 52                                 break;      
Line 53                         }               
Line 54                     }                   
Line 55                 }                       
Line 56                                         
Line 57                 return (T)value;                        
Line 58             }                           
Line 59         }

On line 17 - value = sharedPreferences.GetInt(key, i);- the error occurs because you are trying to read an integer when the shared preference has a long type data.

The solution is to read the shared preference with the following code:

var SyncMasterLast = Preferences.Get("SYNC_MASTER_DATE", (long)0)

So, either the documentation is wrong:

https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/storage/preferences?view=net-maui-8.0&tabs=android

Get preferences To retrieve a value from preferences, you pass the key of the preference, followed by the default value when the key doesn't exist:

Where it says that the second parameter corresponds to the default value, it should say the default value and the type of data to be obtained will be taken from that value.

Or you should fix the code for each of the platform.

Relevant log output

No response

github-actions[bot] commented 3 months ago

Hi I'm an AI powered bot that finds similar issues based off the issue title.

Please view the issues below to see if they solve your problem, and if the issue describes your problem please consider closing this one and thumbs upping the other issue to help us prioritize it. Thank you!

Closed similar issues:

Note: You can give me feedback by thumbs upping or thumbs downing this comment.

Cheesebaron commented 1 month ago

The interface clearly states what the return value is based on:

T Get<T>(string key, T defaultValue, string? sharedName = null);

So in your case, as you found out yourself, you need to use it with the appropriate default value as it controls the return type as well.

Or you should fix the code for each of the platform

How will you infer the type if you are not specifying it explicitly? If you want to cover your behind, then always use the API like:

var myVal = Preferences.Get<YourType>("key", defaultValue);

Is this even a bug?

AugPav commented 1 month ago

The interface clearly states what the return value is based on:

T Get(string key, T defaultValue, string? sharedName = null); So in your case, as you found out yourself, you need to use it with the appropriate default value as it controls the return type as well.

Or you should fix the code for each of the platform

How will you infer the type if you are not specifying it explicitly? If you want to cover your behind, then always use the API like:

var myVal = Preferences.Get("key", defaultValue); Is this even a bug?

It would be clearer if the

Preferences.Get(Shared Name, default value)

was changed to

Preferences.Get(Shared Name, default value, Type)

But that's just my impression.

Cheesebaron commented 1 month ago

It would be clearer if the

Preferences.Get(Shared Name, default value)

was changed to

Preferences.Get(Shared Name, default value, Type)

But that's just my impression.

How would that work, then the signature would be what exactly? What your impression is, is already implied by the signature of T Get<T>(...). You can be explicit and specify a specific type rather than rely on defaultValue to imply the return type and generic type. So instead of writing:

var myValue = Preferences.Get(key, defaultValue);

Then you can achieve what you want with:

var myValue = Preferences.Get<long>(key, defaultValue);

or

long myValue = Preferences.Get(key, defaultValue);