Xamarin.Forms how to backup a SQLite database in SD card - sqlite

I have a SQLite database in my project and I'd like to add a backup function to save that db in the SD card.
Is there any simple way to do this for both Android and IOS using Xamarin.Forms?
I searched among the old (some of them are veeery old) questions but I couldn't find a clear answer.

iOS:
There is no "sdcard" on iOS, but you can copy (File.Copy) the db to the App's Documents directory so it is accessible via the iTunes app so you can copy to your PC/Mac:
Use this directory to store user-generated content. The contents of this directory can be made available to the user through file sharing; therefore, his directory should only contain files that you may wish to expose to the user.
The contents of this directory are backed up by iTunes and iCloud.
So assuming you are using App Support directory for your DB, which you should be:
public string GetDBPath(string dbFileName)
{
// If you are not using App Support directory for your DBs you are doing it wrong on iOS ;-)
var supportDir = NSSearchPath.GetDirectories(NSSearchPathDirectory.ApplicationSupportDirectory, NSSearchPathDomain.User, true)[0];
var dbDir = Path.Combine(supportDir, "database");
if (!Directory.Exists(dbDir))
Directory.CreateDirectory(dbDir);
return Path.Combine(supportDir, dbDir, dbFileName);
}
Copying to your App's doc directory is as simple as:
public void CopyDBToDocs(string dbFileName)
{
var docDir = NSSearchPath.GetDirectories(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomain.User)[0];
File.Copy(GetDBPath(dbFileName), Path.Combine(docDir, dbFileName));
}
Android:
On Android, it can be really simple, or a pain, depending upon the API of the device and what your Android API target of the app that you are developing.
As simple as:
public void CopyDBToSdCard(string dbName)
{
File.Copy(GetDBPath(dbName), Path.Combine(Android.OS.Environment.DirectoryDownloads, dbName);
}
GetDBPath in this case is actually using the "true" database directory which is the subdir from your app's sandboxed "FileDir" location:
public string GetDBPath(string dbFileName)
{
// FYI: GetDatabasePath("") fails on some APIs &| devices &|emulators so hack it... (Some API 23 devices, but not API 21, 27, 28, ...)
var dbPath = context.GetDatabasePath("ZZZ").AbsolutePath.Replace("/ZZZ", "");
if (!Directory.Exists(dbPath))
Directory.CreateDirectory(dbPath);
return context.GetDatabasePath(dbFileName).AbsolutePath;
}
Now to actually perform that file copy to the "external" location from the app, there is the app's manifest permission that is required:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
Also if your app has "targeted" and is running on API-23 (Android 6.0) or above, you need to have the user consent to that permission at runtime.
Using the Android.Support.Compat is the easiest way. Basically you need to check if the permission has already been granted, if not show the user why you need permission and then let the OS ask the user to accept/deny the request. You will then be notified on those perm results.
Note: There is the Forms' Perm plugin from JamesM for the non-bold coders ;-) PermissionsPlugin
void GetExternalPerms()
{
const string writePermission = Manifest.Permission.WriteExternalStorage;
const string readPermission = Manifest.Permission.ReadExternalStorage;
string[] PermissionsLocation =
{
writePermission, readPermission
};
if ((ContextCompat.CheckSelfPermission(this, writePermission) == Permission.Granted) && (ContextCompat.CheckSelfPermission(this, readPermission) == Permission.Granted))
{
return;
}
if (ActivityCompat.ShouldShowRequestPermissionRationale(this, writePermission))
// Displaying a dialog would make sense at this point...
}
ActivityCompat.RequestPermissions(this, PermissionsLocation, 999);
}
Then the results of the user accepting or denying that request will to return via OnRequestPermissionsResult:
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
{
switch (requestCode)
{
case 999: // This is the code that we supply via RequestPermissions
if (grantResults[0] == Permission.Granted)
{
// you have permission, so defer to your DB copy routine now
}
break;
default:
break;
}
}

Related

the file on firebase storage is not accessible if metadata updated

I'm not sure if this is a bug. It works last month and runs into issues a couple of weeks later. I will post a bug report if this issue cannot be resolved.
I have an Android app that allows users to share files with another person via email address. When the file was uploaded to the Firebase Storage successfully, the app pops up a dialog to allow users to type in the address of the recipient for file sharing. And the email address will be written into custom metadata as a key.
In Firebase Storage, each user uploads files to their own folder(email address as folder name). The Storage rules are listed below. The idea is users only can access the files in their own folders, and has read permission for shared files.
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
// read and write permission for owners
match /users/{userEmail}/{allPaths=**} {
allow read, write: if request.auth.token.email == userEmail && request.auth.token.email_verified;
}
// read permission for shared files
match /users/{userEmail}/{allPaths=**} {
allow read: if request.auth != null && request.auth.token.email != userEmail && request.auth.token.email in resource.metadata.keys() && request.auth.token.email_verified;
}
// samples are public to read
match /samples/{allPaths=**} {
allow read;
}
}
}
The rules were modified from this thread.
Firebase rules: dynamically give access to a specific user
To work with the shared files, the app writes the recipient's email address to the file as a key of custom metadata. The Android code for updating metadata is listed below.
private void updateMetadataForSharing(String fileLocation, String documentId, String recipientEmail) {
// write file metadata
StorageMetadata metadata = new StorageMetadata.Builder()
.setCustomMetadata(recipientEmail,"")
.build();
// Update metadata properties
StorageReference storageRef = storage.getReference();
StorageReference fileRef = storageRef.child(fileLocation);
fileRef.updateMetadata(metadata)
.addOnSuccessListener(new OnSuccessListener<StorageMetadata>() {
#Override
public void onSuccess(StorageMetadata storageMetadata) {
// Updated metadata is in storageMetadata
Toast.makeText(ReviewActivity.this, "The file has been shared to "+recipientEmail+", please paste the sharable link from clipboard.", Toast.LENGTH_LONG).show();
String sharableLink = "https://web.app.com/?u="+documentId;
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("sharable link", sharableLink);
clipboard.setPrimaryClip(clip);
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception exception) {
// Uh-oh, an error occurred!
Toast.makeText(ReviewActivity.this, "Error occurred attempting to share the file to "+recipientEmail, Toast.LENGTH_LONG).show();
}
});
}
But the file is not accessible after metadata updated. It was fine if the no metadata written to the file. The web app showed the errors as the picture shown.
web app error message for failing to download the file
I assume it may associate with the access token of file. It has nothing to do with the rules, because it's still not working when I grant all permissions temporarily.
Please advise. Thanks.
I ran into the same problem today with an uploaded file not being accessible after the metadata was updated. It seems like the file becomes inaccessible if the metadata key contains the # character. For some reason the key cannot contain the character but its fine in the value.

How to deal with device settings in Xamarin.Forms?

I have a Xamarin.Forms application supporting Android, iOS, and UWP. I need the application at the starting point to check if location feature is enabled, and if not, suggest the user to do so, and let him open device Settings. But if the user agrees to open the Settings, I would need the application to wait for the results, and then continue based on the newly changed settings. But how can I make the execution wait for the user to finish working with the settings?
Here is the code I have for now:
Task<bool> task = Application.Current?.MainPage?.DisplayAlert("Location service is disabled on this device",
"MyCompany Mobile uses your location to provide you with the correct product mix and other information for your market. Please go into Settings and turn on Location for the device.",
"Settings",
"Maybe Later");
if (task == null)
{
return bu;
}
bool result = await task;
if (result)
{
IDeviceService deviceService = DependencyService.Get<IDeviceService>();
if (deviceService != null)
{
bool openedSuccessfully = await deviceService.OpenDeviceSettingsAsync();
}
}

UWP PreLoaded SQLite

I am trying to copy a preloaded SQLite db into my UWP app. On the initial installation it copies the "test.db", but the size is 0 bytes and there are no tables or data. The original db is 1300 bytes with data and tables.
Another factoid...when I create the app Using Visual Studio 2017 and compile and run/debug the app it works fine, but when I sideload the appx file or download from the Windows Store the db is empty.
Here is the code that I am using:
Task task = CopyDatabase();
private async Task CopyDatabase()
{
bool isDatabaseExisting = false;
try
{
StorageFile storageFile = await ApplicationData.Current.LocalFolder.GetFileAsync("Express.db");
isDatabaseExisting = true;
}
catch
{
isDatabaseExisting = false;
}
if (!isDatabaseExisting)
{
StorageFile databaseFile = await Package.Current.InstalledLocation.GetFileAsync("Express.db");
await databaseFile.CopyAsync(ApplicationData.Current.LocalFolder, "Express.db", NameCollisionOption.ReplaceExisting);
}
}
I'm not getting any error messages.
Does the your database file deployed correctly to the target system?
To confirm it, see your deployed - "Package" - folder. Open command prompt with administrative previleges, and see the directory
c:\Program Files\WindowsApps\your-app-id
If your database file deployed successfully, you can see it in the directory. If not, you may need to change the deploy settings.
To deploy the file to target machine, you should set the property of the one as ...
'BuildAction=Contents'
'Copy to output directory'='Always Copy'
You can set it from solution explorer and right-click the your database file.
If you succeeded the deploying file, your code will copy your database file to app local folder.
c:\Users\YOUR-USER-ID\AppData\Local\Packages\YOUR-APP-ID\LocalState
First, you would need to use await for your CopyDatabase method.
Second, I suggest you call this method in MainPage_Loaded event handler instead of MainPage's Constructor.
public MainPage()
{
this.InitializeComponent();
this.Loaded += MainPage_Loaded;
}
private async void MainPage_Loaded(object sender, RoutedEventArgs e)
{
gui = this; InitializeComponent();
await CopyDatabase();
DataSetup();
CreateNewChartButton.Visibility = Visibility.Collapsed;
SignInButton_Click(null, null);
}

SharedPreferences empty after restarting service / device

As I am working with Google Firebase for Push Notifications, I want to save the Instance Token to the SharedPreferences. Unfortunately, whenever the token gets refreshed and want to check the previous one from SharedPreferences, they are empty...
Is it because I am using a Service here?
public class MyFirebaseIIDService : FirebaseInstanceIdService
{
public override void OnTokenRefresh()
{
var sharedPreferences = PreferenceManager.GetDefaultSharedPreferences(this);
var sharedPreferencesEditor = sharedPreferences.Edit();
// Get Firebase Instance Token
var refreshedToken = FirebaseInstanceId.Instance.Token;
// Check if a Firebase Instance Token has been registered before and unregister it
var oldToken = sharedPreferences.GetString("FirebaseInstanceToken", null);
if (oldToken != null) // is ALWAYS null :(
{
// Unregister old token...
}
// Save the Firebase Instance Token locally
sharedPreferencesEditor.PutString("FirebaseInstanceToken", refreshedToken);
sharedPreferencesEditor.Apply();
// At this point, the SharedPreferences have to token saved.
// Next time, the app reaches this point, it is gone...
}
}
Sorry for the syntax confusion, I use Xamarin, so this is C# but it should not make any difference.
I don't know how it work in xamarin but in native android getSharedPreferences from service context maybe wrong. You should use only applicationContext or MODE_MULTI_PROCESS when open shared preferences.
You can see similar question here.
Xamarin and C# also have this mode when your open some file, so i think exactly the same with preferences. Try some like this instead of using GetDefaultSharedPreferences:
ISharedPreferences prefs = Application.Context.GetSharedPreferences ("PREF_NAME", FileCreationMode.MultiProcess);
Did you uninstall your app to refresh you token? If yes I think you can not get the old token because the SharedPreferences is cleared when you uninstall your app.
I have tried java code to save the token :
public void onTokenRefresh() {
// Get updated InstanceID token.
String refreshedToken = FirebaseInstanceId.getInstance().getToken();
Log.d("Mike", "Refreshed token: " + refreshedToken);
SharedPreferences mSharedPreferences = getSharedPreferences("hello",0);
String oldToken = mSharedPreferences.getString("Token",null);
if(oldToken == null)
{
Log.d("Mike", "oldToken: " + null);
}
SharedPreferences.Editor mEditor = mSharedPreferences.edit();
mEditor.putString("Token", refreshedToken);
mEditor.commit();
// TODO: Implement this method to send any registration to your app's servers.
//sendRegistrationToServer(refreshedToken);
}
At first time you install your app you will get the token and save it in the SharedPreferences, next time you open your app and show the old token in the textview then you can find your token has been saved , And do not uninstall your app:
TextView tv1 = (TextView)findViewById(R.id.tv1);
SharedPreferences mSharedPreferences = getSharedPreferences("hello",0);
String oldToken = mSharedPreferences.getString("Token",null);
tv1.setText(oldToken);
It works. But when you uninstall your app the textview shows null.
I solved the problem and it turned out, that my code was working correctly but behaved strange on my test device.
After re-building the application, SharedPreferences have been cleared, although I checked the Preserve application data/cache on device between deploys option in Visual Studio. That was because my physical testing device was rooted and did not accept this option.
When trying on an unrooted device, everything worked as expected.

android marshmallow - SMS_RECEIVED permission

i recently updated my app to support android 6 marshmallow.
i followed the instruction on https://developer.android.com/training/permissions/requesting.html
and added requestPermissions for Manifest.permission.RECEIVE_SMS.
when im runing the following code :
Log.i(TAG, "sending SMS...");
Intent intent = new Intent("android.provider.Telephony.SMS_RECEIVED");
intent.putExtra("pdus", data);
getContext().sendOrderedBroadcast(intent, null);
i get
java.lang.SecurityException: Permission Denial: not allowed to send broadcast android.provider.Telephony.SMS_RECEIVED from pid=1999, uid=10056
i cant send sms broadcast on the device even if i grant SMS_RECEIVED permission.
any idea why i get this security exception on android 6.
my goal is to generate a fake sms in my device link[can I send "SMS received intent"?
. i didnt find any mentions on google that its not permitted anymore .
You need to add the permission into manifest xml:
<uses-permission android:name="android.permission.RECEIVE_SMS"></uses-permission>
AND, you need to ask for the permission at runtime. Until android 6, the permissions were granted automatically on installation. In android 6 and above, you can install application and not grant the permission. You can use this function in your activity class:
private void requestSmsPermission() {
String permission = Manifest.permission.RECEIVE_SMS;
int grant = ContextCompat.checkSelfPermission(this, permission);
if ( grant != PackageManager.PERMISSION_GRANTED) {
String[] permission_list = new String[1];
permission_list[0] = permission;
ActivityCompat.requestPermissions(this, permission_list, 1);
}
}
this - your activity.
The Android 6 runtime permission android.provider.Telephony.SMS_RECEIVED gives you permission to receive that message when it is sent by the system SMS provider.
You however are trying to broadcast that message yourself. I'm not sure that is permitted, and as you have found is not controlled by the same permission. (In fact, I assume that it has been locked down on Marshmallow so that only the system is able to notify apps of received SMS messages).
You need a permission for api level 23+, google reworked the permission system so the app user can grant and revoke permissions after installing your app
final private int REQUEST_CODE_ASK_PERMISSIONS = 123;
if(Build.VERSION.SDK_INT < 23){
//your code here
}else {
requestContactPermission();
}
private void requestContactPermission() {
int hasContactPermission =ActivityCompat.checkSelfPermission(context,Manifest.permission.RECEIVE_SMS);
if(hasContactPermission != PackageManager.PERMISSION_GRANTED ) {
ActivityCompat.requestPermissions(Context, new String[] {Manifest.permission.RECEIVE_SMS}, PERMISSION_REQUEST_CODE);
}else {
//Toast.makeText(AddContactsActivity.this, "Contact Permission is already granted", Toast.LENGTH_LONG).show();
}
}
#Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_ASK_PERMISSIONS:
// Check if the only required permission has been granted
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.i("Permission", "Contact permission has now been granted. Showing result.");
Toast.makeText(this,"Contact Permission is Granted",Toast.LENGTH_SHORT).show();
} else {
Log.i("Permission", "Contact permission was NOT granted.");
}
break;
}
}
You must grant SMS permission to your app after installation. Just go to
Settings > Apps > Your_app > Permissions
and then grant the required permission.
Android 6.0 / SDK 23 introduces a new way of requesting permissions.
You need to request the SMS permission, see the link below for how to handle permissions:
https://developer.android.com/training/permissions/index.html

Resources