In this article, I will model a class depending on the operation of the well-known Windows INI files. Of course, Windows own system will be used in the Windows implementation. On Android side, I will use the SharedPreferences API which is very close.
INI file concept
In an INI file, you have Key-Value pairs organized by sections. You can read or write values.
Under Windows, the INI file format is a simple text file with a Key=Value per line. All Key-Value pairs related to the same section are grouped under a header line in the form of the section name between brackets.
Under Android, the file format is not specified. You are not supposed to access the file directly. You use a “Shared Preference Editor” to access it. Android API lacks the “section” concept we have in Windows. This is not a problem. To create the section concept, I will simply prefix each key by his section name surrounded by brackets like this: ‘[‘ + Section + ‘]_’ + Key
Delphi TIniFile revisited
Since the beginning, Delphi has a class encapsulation Windows INI files. It is well named “TIniFile” and sits into “System.IniFiles” unit.
I will use the same class name in my implementation and even the save class signature by inheriting from the existing TCustomIniFile for Android and TIniFile for Windows.
Using the same class name as an existing one will force you to pay some attention to the units used in the uses clause, and/or prefix the class name you intent to use with the unit name.
I made things simples. Under both Windows and Android, in your application, you do not use System.IniFiles but FMX.Overbyte.IniFiles. No other change is required. Your application will compile targeted for Windows as well as Android. The conditional compilation is located in FMX.Overbyte.IniFiles and you can safely ignore it!
Storage location
TIniFile constructor takes a filename as argument. This will be the file where the sections and key-value pairs will be stored. The Windows API store the file exactly where you specify it when using a full path. When you omit the path, Windows tore the file in the Windows directory. Since Windows Vista, normal user cannot write to the Windows directory. So it fails.
I slightly changed the base class so that when a full path is omitted, the INI file is stored in the user profile LoaclAppData special directory (non-roaming version). This is a convenient place most of the time. You may always specify a full path name if you want to store it elsewhere.
Android has a “well known” place to store the preference files. We are not supposed to know where. The actual files are not available directly unless your Android device is rooted.
TIniFile constructor in the Android implementation will simple ignore any path you specify and let Android API store the file where it want it to be stored. This could cause a problem if you want to use the same file name for different files stored in different folders. This will cause trouble since the path is ignored.
Windows implementation
The windows implementation is quite trivial since it already exists in Delphi RTL. As stated above, I derived my class from Delphi existing class and only override the constructor to adjust the path when left empty.
The resulting declaration is trivial:
TIniFile = class(System.IniFiles.TIniFile) public constructor Create(const AFileName : String); end;
The implementation is simple:
constructor TIniFile.Create(const AFileName: String); var FileName : String; Path : array [0..1023] of Char; AppExeName : array [0..1023] of Char; AppName : String; LocalAppData : String; begin if ExtractFilePath(AFileName) = '' then begin GetModuleFileName(0, AppExeName, Sizeof(AppExeName)); SHGetFolderPath(0, CSIDL_LOCAL_APPDATA, 0, SHGFP_TYPE_CURRENT, @Path[0]); AppName := ChangeFileExt(ExtractFileName(AppExeName), ''); LocalAppData := IncludeTrailingPathDelimiter(Path) + CompanyFolder + '\' + AppName + '\'; FileName := LocalAppData + AFileName; ForceDirectories(LocalAppData); end else FileName := AFileName; inherited Create(FileName); end;
This implementation makes use of SHgetFolderPath API function to get the special directory “LocalAppData” located in each user profile. I used ForceDirectories to create the directory if it does not already exist.
You may want to change the location by changing the constant CSIDL_LOCAL_APPDATA to another one (There is a bunch of such constant, see the API documentation or Delphi source code if you have an edition which includes it).
You may also want to change the string constant “CompanyFolder” to your actual company name instead of OverByte which is my company name.
Using the demo application named “IniFileDemo”, running under Win7, the INI files without path will be stored in “C:\Users\
Android implementation
Android implementation makes use of SharedPreferences API which is already defined by Delphi runtime library. You handle that API using an interface named “JSharedPreferences” which is located in Androidapi.JNI.GraphicsContentViewText.
We need to implement most of the TIniFile methods. We can skip the read/write for other data types than string because they are all based on the read/write string.
The class declaration looks like this:
TIniFile = class(System.IniFiles.TCustomIniFile) private FPrefs : JSharedPreferences; function InitPrefs : JSharedPreferences; function Key(const Section, Ident : String) : JString; procedure ReadSectionKeysValues(const Section : String; const KeyOnly : Boolean; Strings : TStrings); public constructor Create(const FileName: String); function ReadString(const Section, Ident, Default: String): String; override; procedure WriteString(const Section, Ident, Value: String); override; procedure ReadSection(const Section: String; Strings: TStrings); override; procedure ReadSections(Strings: TStrings); override; procedure ReadSectionValues(const Section: String; Strings: TStrings); override; procedure DeleteKey(const Section, Ident: String); override; procedure EraseSection(const Section: string); override; procedure UpdateFile; override; end;
The class TIniFile derives from existing TCustomIniFile. I used the fully qualified class name to avoid confusion (Here it is not strictly necessary since we do not redefine TCustomIniFile).
All the public methods are those required to make TIniFile work as it does under Windows. Private members are required as helpers for the implementation. As their visibility implies, you will never directly use them.
All methods need to get hand on a JSharedPreferences interface. That is why I created a member variable FPrefs to store it and an InitPrefs method to initialize it.
Once you get FPrefs, you may use it to fetch a value. look at ReadString implementation:
function TIniFile.ReadString(const Section, Ident, Default: String): String; begin InitPrefs; Result := JStringToString(FPrefs.GetString(Key(Section, Ident), StringToJString(Default))); end;
FPrefs.GetString is themethod use to retrieve (read) a stored value given his key. Here, as explained above, we implement the concept of section, so the key is really constructed using the section name and the identifier used outside of the class as key.
JStringToString and StringToJString are support functions to marshal back and forth a Delphi string to a Java string (Remember Android API is written in Java).
ReadSection, ReadSections and ReadSectionValues all require to enumerate all keys are save values in a string list for some of the keys if they match a condition. Iterating all the keys is a common process so I moved it to a specialized private method ReadSectionKeysValues.
Here is the implementation:
procedure TIniFile.ReadSectionKeysValues( const Section : String; // Section to read, or empty for keys and values const KeyOnly : Boolean; Strings : TStrings); var AMap : JMap; ASet : JSet; AIter : JIterator; AObj : JObject; AString : JString; DString : String; ASection : String; AIdent : String; I, J : Integer; begin if not Assigned(Strings) then Exit; InitPrefs; Strings.Clear; AMap := FPrefs.GetAll; if not Assigned(AMap) then Exit; ASet := AMap.entrySet; if not Assigned(ASet) then Exit; AIter := ASet.iterator; Strings.BeginUpdate; while AIter.hasNext do begin AObj := AIter.next; AString := AObj.toString; DString := JStringToString(AString); // We get "[Section]_Ident" if (Length(DString) > 3) and (DString[Low(DString)] = '[') then begin I := Pos(']', DString); if I > 0 then begin ASection := Copy(DString, 2, I - 2); if Section = '' then begin // We are reading section names if Strings.IndexOf(ASection) < 0 then Strings.Add(ASection); end else if SameText(Section, ASection) then begin // We are reading the key names (Ident) if KeyOnly then J := PosEx('=', DString) else J := Length(DString) + 1; if J > 0 then begin AIdent := Copy(DString, I + 2, J - I - 2); Strings.Add(AIdent); end; end; end; end; end; Strings.EndUpdate; end;
SharedPreferences Android API make use of string collection returned by getAll method to store all the preferences values. It is a generic Java class which can be accessed using a JMap interface which is available to Delphi program. Accessing the individual strings is 4 steps process:
1) Get the JMap interface by calling getAll
2) Get the JSet interface on behalf f the JMap
3) Get the JIterator on behalf og the JSet
4) Iterate with the JIterator to get hand of all object in the collection
The objects are here JStrings we can convert to Delphi string and process them.
The enumerated strings looks like this: “[Section1]_Key1=Value1”. We can then easily parse the string to extract the parts and do whatever we need with it.
The rest of the class implementation is quite trivial.
Full source code
The source code as well as a demo application is available from my website athttp://www.overbyte.be/frame_index.html?redirTo=/blog_source_code.html
FMX.Overbyte.IniFiles.pas
unit FMX.Overbyte.IniFiles; {$DEFINE OVERBYTE_INCLUDE_MODE} {$IFDEF ANDROID} {$I FMX.Overbyte.Android.IniFiles.pas} {$ENDIF} {$IFDEF MSWINDOWS} {$I FMX.Overbyte.Windows.IniFiles.pas} {$ENDIF}FMX.Overbyte.Windows.IniFiles.pas
{$IFNDEF OVERBYTE_INCLUDE_MODE} unit FMX.Overbyte.Windows.IniFiles; {$ENDIF} interface uses System.SysUtils, System.Classes, System.IniFiles, WinApi.Windows, WinApi.ShlObj; const CompanyFolder = 'OverByte'; type // We are enhancing Embarcadero implementation TIniFile = class(System.IniFiles.TIniFile) public constructor Create(const AFileName : String); end; implementation { TIniFile } constructor TIniFile.Create(const AFileName: String); var FileName : String; Path : array [0..1023] of Char; AppExeName : array [0..1023] of Char; AppName : String; LocalAppData : String; begin // When the path is empty, Windows use Windows directory (C:\windows). This // is bad since Win7 which requires special permission to write to this // directory. // This implementation redirect the INI file to the user profile, that is //\Local Settings\Application Data (non roaming) // If you really want to write to Windows directory, then you must // specify that path name specifically. if ExtractFilePath(AFileName) = '' then begin GetModuleFileName(0, AppExeName, Sizeof(AppExeName)); SHGetFolderPath(0, CSIDL_LOCAL_APPDATA, 0, SHGFP_TYPE_CURRENT, @Path[0]); AppName := ChangeFileExt(ExtractFileName(AppExeName), ''); LocalAppData := IncludeTrailingPathDelimiter(Path) + CompanyFolder + '\' + AppName + '\'; FileName := LocalAppData + AFileName; ForceDirectories(LocalAppData); end else FileName := AFileName; inherited Create(FileName); end; end.
FMX.Overbyte.Android.IniFiles.pas
{$IFNDEF OVERBYTE_INCLUDE_MODE} unit FMX.Overbyte.Android.IniFiles; {$ENDIF} interface uses System.SysUtils, System.Classes, System.IniFiles, System.StrUtils, FMX.Helpers.Android, Androidapi.NativeActivity, Androidapi.JNI, Androidapi.JNI.App, Androidapi.JNI.GraphicsContentViewText, Androidapi.JNI.JavaTypes; type TIniFile = class(System.IniFiles.TCustomIniFile) private FPrefs : JSharedPreferences; function InitPrefs : JSharedPreferences; function Key(const Section, Ident : String) : JString; procedure ReadSectionKeysValues(const Section : String; const KeyOnly : Boolean; Strings : TStrings); public constructor Create(const FileName: String); function ReadString(const Section, Ident, Default: String): String; override; procedure WriteString(const Section, Ident, Value: String); override; procedure ReadSection(const Section: String; Strings: TStrings); override; procedure ReadSections(Strings: TStrings); override; procedure ReadSectionValues(const Section: String; Strings: TStrings); override; procedure DeleteKey(const Section, Ident: String); override; procedure EraseSection(const Section: string); override; procedure UpdateFile; override; end; implementation { TIniFile } constructor TIniFile.Create(const FileName: String); begin // Under Android, just ignore the path part because Android has a well // known place to store preferences files inherited Create(ExtractFileName(FileName)); end; procedure TIniFile.DeleteKey(const Section, Ident: String); var Edit : JSharedPreferences_Editor; begin InitPrefs; Edit := FPrefs.Edit; Edit.Remove(Key(Section, Ident)); Edit.Apply; end; procedure TIniFile.EraseSection(const Section: String); var Idents : TStringList; Edit : JSharedPreferences_Editor; I : Integer; begin Idents := TStringList.Create; ReadSectionKeysValues(Section, TRUE, Idents); InitPrefs; Edit := FPrefs.Edit; for I := 0 to Idents.Count - 1 do Edit.Remove(Key(Section, Idents[I])); Edit.Apply; end; function TIniFile.InitPrefs : JSharedPreferences; begin if not Assigned(FPrefs) then FPrefs := SharedActivityContext.getSharedPreferences( StringToJString(FileName), TJActivity.JavaClass.MODE_PRIVATE); Result := FPrefs; end; function TIniFile.Key(const Section, Ident: String): JString; begin Result := StringToJString('[' + Section + ']_' + Ident); end; procedure TIniFile.ReadSection(const Section: String; Strings: TStrings); begin if Section = '' then begin if Assigned(Strings) then Strings.Clear; end else ReadSectionKeysValues(Section, TRUE, Strings); end; procedure TIniFile.ReadSections(Strings: TStrings); begin ReadSectionKeysValues('', FALSE, Strings); end; procedure TIniFile.ReadSectionKeysValues( const Section : String; // Section to read, or empty for keys and values const KeyOnly : Boolean; Strings : TStrings); var AMap : JMap; ASet : JSet; AIter : JIterator; AObj : JObject; AString : JString; DString : String; ASection : String; AIdent : String; I, J : Integer; begin if not Assigned(Strings) then Exit; InitPrefs; Strings.Clear; AMap := FPrefs.GetAll; if not Assigned(AMap) then Exit; ASet := AMap.entrySet; if not Assigned(ASet) then Exit; AIter := ASet.iterator; Strings.BeginUpdate; while AIter.hasNext do begin AObj := AIter.next; AString := AObj.toString; DString := JStringToString(AString); // We get "[Section]_Ident" if (Length(DString) > 3) and (DString[Low(DString)] = '[') then begin I := Pos(']', DString); if I > 0 then begin ASection := Copy(DString, 2, I - 2); if Section = '' then begin // We are reading section names if Strings.IndexOf(ASection) < 0 then Strings.Add(ASection); end else if SameText(Section, ASection) then begin // We are reading the key names (Ident) if KeyOnly then J := PosEx('=', DString) else J := Length(DString) + 1; if J > 0 then begin AIdent := Copy(DString, I + 2, J - I - 2); Strings.Add(AIdent); end; end; end; end; end; Strings.EndUpdate; end; procedure TIniFile.ReadSectionValues(const Section: String; Strings: TStrings); begin if Section = '' then Strings.Clear else ReadSectionKeysValues(Section, FALSE, Strings); end; function TIniFile.ReadString(const Section, Ident, Default: String): String; begin InitPrefs; Result := JStringToString(FPrefs.GetString(Key(Section, Ident), StringToJString(Default))); end; procedure TIniFile.UpdateFile; begin // Nothing to do end; procedure TIniFile.WriteString(const Section, Ident, Value: String); var Edit : JSharedPreferences_Editor; begin InitPrefs; Edit := FPrefs.Edit; Edit.PutString(Key(Section, Ident), StringToJString(Value)); Edit.Apply; end; end.
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
This article is available from http://francois-piette.blogspot.be
7 comments:
It should be mentioned that TMemIniFile is already cross platform and on platforms different than Windows TIniFile already maps to TMemIniFile instead.
TMemIniFile is not really an ini file alotough you could store the content to a file. And under Android, it is unrelated to the API designed to save the preferences. IMO, TMemIniFile could be useful but not for the purpose I intend my ini file implementation using the SharedPreferences Android API.
Stop Working in xe7 =(
Xe7 + Kitkat = OK
Xe7 + Lollipop = Close apk
Thank you very much for this implementation, it has saved me in my project.
Lollipop = Close apk confirmed
RS Berlin :
it should be
FPrefs := TAndroidHelper.Context.getSharedPreferences(
StringToJString(FileName),
TJContext.JavaClass.MODE_PRIVATE);
MODE_PRIVATE isn't a member of TJActivity in Android 5 and 6
Post a Comment