Just getting into the Glide image loading library for Android.
Working with code from here: https://github.com/bumptech/glide/issues/459
Full project here:
https://github.com/mhurwicz/glide02
I'm getting the following exception when I run the app in the emulator in Android Studio:
java.lang.IllegalArgumentException: Failed to find configured root that contains /data/data/com.example.glide02/cache/image_manager_disk_cache/5a992029460eed14244e8b970d969d45518b2f7ac10f71eb26bd0aaf7c3bcf06.0
The rest of the messages are:
at android.support.v4.content.FileProvider$SimplePathStrategy.getUriForFile(FileProvider.java:711)
at android.support.v4.content.FileProvider.getUriForFile(FileProvider.java:400)
at com.example.glide02.ShareTask.onPostExecute(ShareTask.java:37)
at com.example.glide02.ShareTask.onPostExecute(ShareTask.java:15)
at android.os.AsyncTask.finish(AsyncTask.java:651)
at android.os.AsyncTask.-wrap1(AsyncTask.java)
at android.os.AsyncTask$InternalHandler.handleMessage(AsyncTask.java:668)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
The error occurs on FileProvider.getUriForFile()
#Override protected void onPostExecute(File result) {
if (result == null) { return; }
Uri uri = FileProvider.getUriForFile(context, context.getPackageName(), result);
share(uri); // startActivity probably needs UI thread
}
I've looked at several other questions relating to this error, but can't see how they relate to my case, perhaps due to my lack of familiarity with this whole area.
Any help will be greatly appreciated.
This is the full code for the class in which the above method occurs:
class ShareTask extends AsyncTask<String, Void, File> {
private final Context context;
public ShareTask(Context context) {
this.context = context;
}
#Override protected File doInBackground(String... params) {
String url = params[0]; // should be easy to extend to share multiple images at once
try {
return Glide
.with(context)
.load(url)
.downloadOnly(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
.get() // needs to be called on background thread
;
} catch (Exception ex) {
Log.w("SHARE", "Sharing " + url + " failed", ex);
return null;
}
}
#Override protected void onPostExecute(File result) {
if (result == null) { return; }
Uri uri = FileProvider.getUriForFile(context, context.getPackageName(), result);
share(uri); // startActivity probably needs UI thread
}
private void share(Uri result) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("image/jpeg");
intent.putExtra(Intent.EXTRA_SUBJECT, "Shared image");
intent.putExtra(Intent.EXTRA_TEXT, "Look what I found!");
intent.putExtra(Intent.EXTRA_STREAM, result);
context.startActivity(Intent.createChooser(intent, "Share image"));
}
}
In my case, this was the solution in filepath.xml
<paths>
<root-path name="root" path="." />
</paths>
In filepaths.xml you need
<cache-path name="shared_images_from_glide_cache" path="image_manager_disk_cache"/>
This will result in mapping
new File("/data/data/com.example.glide02/cache/image_manager_disk_cache/5a992029460eed14244e8b970d969d45518b2f7ac10f71eb26bd0aaf7c3bcf06.0")
to
Uri.parse("content://com.example.glide02/shared_images_from_glide_cache/5a992029460eed14244e8b970d969d45518b2f7ac10f71eb26bd0aaf7c3bcf06.0")
(Not sure if I got it exactly right, but you should get the point.)
When I was trying to get Uri for the file stored in an external SD card, I was getting this error. but when I do the same set of operations with files stored in internal storage, it worked fine. In my case making the below-mentioned changes in the resource file which is #xml/file_path(you can check yours under meta-data tag of the provider in the manifest.)
//adding the below line will give you a warning saying that Element root-path is not allowed here
//but you can ignore that.
<paths>
<root-path name="root" path="." />
</paths
The below code is for reference.
//#xml/file_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="files" path="." />
<external-files-path name="external_files" path="." />
<external-path name="external_files" path="." />
<cache-path name="cached_files" path="." />
<external-cache-path name="cached_files" path="." />
<root-path name="root" path="." />
</paths>
//My provider in Manifest.xml
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/file_paths" />
</provider>
Tested with android 11, 10 and 7
Related
I have a task to create a file and send it by email through a mail application. I have fileProvider in AndroidManifest.xml
<application
... >
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.test.example.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/provider_paths" />
</provider>
...
</application>
I also added paths to provider_paths is
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external-path"
path="."/>
<external-files-path
name="external-files-path"
path="."/>
<files-path
name="files_path"
path="."/>
</paths>
Then I create a file and give it through the URI. Next, I create an intent to send a letter via email
suspend fun collectLogsToFile(): Uri {
val logcatFile = File(context.filesDir, "logcat.log")
withContext(Dispatchers.IO) { BufferedOutputStream(FileOutputStream(logcatFile.path)).use { stream ->
stream.write("test".toByteArray())
}
}
if (!logcatFile.exists()) {
withContext(Dispatchers.IO) {
logcatFile.createNewFile()
}
}
return FileProvider.getUriForFile(
context,
"com.test.example.fileprovider",
logcatFile
)
}
private suspend fun createEmailIntent(): Intent {
val logFileUri = collectLogsToFile()
return Intent(Intent.ACTION_SENDTO).apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
data = Uri.parse("mailto:")
putExtra(
Intent.EXTRA_EMAIL,
arrayOf(DEVELOPERS_EMAIL)
)
putExtra(
Intent.EXTRA_TEXT,
"text"
)
putExtra(
Intent.EXTRA_SUBJECT,
"subject"
)
putExtra(
Intent.EXTRA_STREAM,
logFileUri
)
}
}
but I, when I open the Google mail app or another mail app, I get error:
E Failed to find provider info for com.test.example.fileprovider
E ComposeActivity: Error adding attachment [CONTEXT
geq: FileNotFoundException when openAssetFileDescriptor.
at fyx.f(PG:28)
at gmz.I(PG:4)
at gll.run(PG:59)
at gmz.at(PG:31)
at gmz.as(Unknown Source:6)
at fyz.a(PG:86)
at aswv.c(PG:2)
at aswx.run(PG:9)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:462)
at aqkz.run(PG:3)
at airh.run(PG:81)
at aqjm.run(PG:29)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:246)
at android.app.ActivityThread.main(ActivityThread.java:8653)
at java.lang.reflect.Method.invoke(Native Method)
at
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)
Caused by: java.io.FileNotFoundException: No content provider:
content://com.test.example.fileprovider/files_path/logcat.log
at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:1979)
at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:1808)
at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:1724)
at fyx.f(PG:23)
at gmz.I(PG:4)
at gll.run(PG:59)
at gmz.at(PG:31)
at gmz.as(Unknown Source:6)
at fyz.a(PG:86)
at aswv.c(PG:2)
at aswx.run(PG:9)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:462)
at aqkz.run(PG:3)
at airh.run(PG:81)
at aqjm.run(PG:29)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:246)
at android.app.ActivityThread.main(ActivityThread.java:8653)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)
But I always get the fileContentProvider error. Tell me, please, what am I doing wrong? I was looking for a lot of similar questions and did. as I seem to think correctly
What am I doing wrong? Thanks
I'm working on a Xamarin.Form App (Android and iOs) which need to get AAID and IDFA.
I started with Android part, I implemented AdvertisingIdClient.GetAdvertisingIdInfo but this function return Timeout error (about after 10s)
I use DependencyService from Form to get AAID from Android project and I compile with Android 11.0
I followed internet examples to achieve it but I'm quite sure I missed something or did something wrong, but what?
What I did :
Added nuget Xamarin.GooglePlayServices.Ads (120.3.0.3) and Xamarin.GooglePlayServices.Basement (117.6.0.4), should be latest compliant with Android 11
Created Interface for dependency service and Created cs in Android project
In Form I call my Dependencyservice function using Task.Run to run i in background (not sure it's the rigth way, but function run, otherwise function raise an error)
IDeviceAdIdentifier service = DependencyService.Get<IDeviceAdIdentifier>();
var tadid = Task.Run(() =>
{
var t = service.GetAdvertisingIdentifier();
t.Wait();
return t.Result;
});
tadid.Wait();
Idfa = tadid.Result;
In Android I save Context from MainActivity
In Android Service I have :
[assembly: Dependency(typeof(TestAdId.Droid.Services.DeviceAdIdentifier))]
namespace TestAdId.Droid.Services
{
public class DeviceAdIdentifier : TestAdId.Services.IDeviceAdIdentifier
{
public async Task<string> GetAdvertisingIdentifier()
{
if (MainActivity.Context == null) return "";
string id = "";
try
{
AdvertisingIdClient.SetShouldSkipGmsCoreVersionCheck(true);
var adinfo = AdvertisingIdClient.GetAdvertisingIdInfo(MainActivity.Instance); // Also tested with MainActivity.Context which is Application context saved during init
if (adinfo != null)
return adinfo.Id;
}
catch (IOException ioerr)
{
return ioerr.Message;
}
catch (Exception err)
{
return err.Message;
}
return "";
}
}
}
In my manifest :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.testadid" android:installLocation="auto">
<uses-sdk android:minSdkVersion="25" android:targetSdkVersion="30" />
<application android:label="TestAdId.Android" android:theme="#style/MainTheme" android:icon="#drawable/xamarin_logo">
<meta-data android:name="com.google.android.gms.ads.AD_MANAGER_APP" android:value="true" />
<meta-data android:name="com.google.android.gms.version" android:value="#integer/google_play_services_version" />
</application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
The error raised on my Samsung Galaxy A71 is :
Message : java.util.concurrent.TimeoutException: Timed out waiting for the service connection
StackTrace :
at Java.Interop.JniPeerMembers+JniStaticMethods.InvokeObjectMethod (System.String encodedMember, Java.Interop.JniArgumentValue* parameters) [0x00018] in <1921523bc22e407fa9a0855bdae29335>:0
at Google.Ads.Identifier.AdvertisingIdClient.GetAdvertisingIdInfo (Android.Content.Context context) [0x00027] in /Users/runner/work/1/s/generated/com.google.android.gms.play-services-ads-identifier/obj/Release/monoandroid90/generated/src/Google.Ads.Identifier.AdvertisingIdClient.cs:171
at TestAdId.Droid.Services.DeviceAdIdentifier.GetAdvertisingIdentifier () [0x00032] in C:\Working\Digital\Sources\TestAdId\TestAdId\TestAdId.Android\Services\DeviceAdIdentifier.cs:30
--- End of managed Java.IO.IOException stack trace ---
java.io.IOException: java.util.concurrent.TimeoutException: Timed out waiting for the service connection
at com.google.android.gms.ads.identifier.AdvertisingIdClient.zzb(com.google.android.gms:play-services-ads-identifier##17.0.1:16)
at com.google.android.gms.ads.identifier.AdvertisingIdClient.getAdvertisingIdInfo(com.google.android.gms:play-services-ads-identifier##17.0.1:3)
Caused by: java.util.concurrent.TimeoutException: Timed out waiting for the service connection
at com.google.android.gms.common.BlockingServiceConnection.getServiceWithTimeout(com.google.android.gms:play-services-basement##17.6.0:4)
at com.google.android.gms.ads.identifier.AdvertisingIdClient.zzb(com.google.android.gms:play-services-ads-identifier##17.0.1:14)
... 1 more
I tested an application to retrieve AAID and it works on my phone.
So if someone can help me it will be great. Tell me if you need more information/code
Thanks
Hervé
It's just a POC so nothing really special in MainActivity. I created a standard xamarin.Forms project and put my code in about page in a button
Android MainActivity.cs
using System;
using Android.App;
using Android.Content.PM;
using Android.Runtime;
using Android.OS;
using Android.Content;
namespace TestAdId.Droid
{
[Activity(Label = "TestAdId", Icon = "#mipmap/icon", Theme = "#style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize )]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
public static Context Context =null;
public static MainActivity Instance = null;
protected override void OnCreate(Bundle savedInstanceState)
{
Instance = this;
base.OnCreate(savedInstanceState);
Context = Android.App.Application.Context;
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
LoadApplication(new App());
}
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{
Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
}
The About Screen xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="TestAdId.Views.AboutPage"
xmlns:vm="clr-namespace:TestAdId.ViewModels"
Title="{Binding Title}">
<ContentPage.BindingContext>
<vm:AboutViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<ResourceDictionary>
<Color x:Key="Accent">#96d1ff</Color>
</ResourceDictionary>
</ContentPage.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackLayout BackgroundColor="{StaticResource Accent}" VerticalOptions="FillAndExpand" HorizontalOptions="Fill">
<StackLayout Orientation="Horizontal" HorizontalOptions="Center" VerticalOptions="Center">
<ContentView Padding="0,40,0,40" VerticalOptions="FillAndExpand">
<Image Source="xamarin_logo.png" VerticalOptions="Center" HeightRequest="64" />
</ContentView>
</StackLayout>
</StackLayout>
<ScrollView Grid.Row="1">
<StackLayout Orientation="Vertical" Padding="30,24,30,24" Spacing="10">
<Label Text="Start developing now" FontSize="Title"/>
<Label Text="Make changes to your XAML file and save to see your UI update in the running app with XAML Hot Reload. Give it a try!" FontSize="16" Padding="0,0,0,0"/>
<Label FontSize="16" Padding="0,24,0,0">
<Label.FormattedText>
<FormattedString>
<FormattedString.Spans>
<Span Text="Learn more at "/>
<Span Text="https://aka.ms/xamarin-quickstart" FontAttributes="Bold"/>
</FormattedString.Spans>
</FormattedString>
</Label.FormattedText>
</Label>
<Label x:Name="IdfaLabel" Text="Unknown" TextColor="Black" FontSize="16" ></Label>
<Button Margin="0,10,0,0" Text="Get IDFA"
Clicked="Button_Clicked"
BackgroundColor="{StaticResource Primary}"
TextColor="White" />
</StackLayout>
</ScrollView>
</Grid>
</ContentPage>
The About Form Code
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using TestAdId.Services;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using System.Net;
namespace TestAdId.Views
{
public partial class AboutPage : ContentPage
{
public string Idfa = "";
public AboutPage()
{
InitializeComponent();
}
private void Button_Clicked(object sender, EventArgs e)
{
IdfaLabel.Text = "Getting IDFA...";
IDeviceAdIdentifier service = DependencyService.Get<IDeviceAdIdentifier>();
var tadid = Task.Run(() =>
{
var t = service.GetAdvertisingIdentifier();
t.Wait();
return t.Result;
});
tadid.Wait();
Idfa = tadid.Result;
IdfaLabel.Text = Idfa;
}
}
}```
Tell me if you need another thing. This is a really simple sample just to try to making AAID working
Thanks
Regards
Hervé
My -app.xml file is a simple,
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<application xmlns="http://ns.adobe.com/air/application/4.0">
<id>com.sixminute.plaintester</id>
<filename>OtherTester</filename>
<name>OtherTester</name>
<versionNumber>0.0.0</versionNumber>
<initialWindow>
<content>[This value will be overwritten by Flash Builder in the output app.xml]</content>
<!--
<aspectRatio>portrait</aspectRatio>
-->
<renderMode>direct</renderMode>
<autoOrients>true</autoOrients>
<fullScreen>true</fullScreen>
<visible>true</visible>
</initialWindow>
<android>
<manifestAdditions><![CDATA[
<manifest android:installLocation="auto">
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="19" />
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
]]></manifestAdditions>
</android>
<iPhone>
<InfoAdditions><![CDATA[
]]></InfoAdditions>
<requestedDisplayResolution>high</requestedDisplayResolution>
</iPhone>
</application>
And the entry point file, is just
package
{
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.text.TextField;
public class OtherTester extends Sprite
{
public function OtherTester()
{
super();
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;
var tx:TextField = new TextField();
tx.text = 'hello world';
tx.x = tx.y = 10;
addChild(tx);
trace(stage.supportedOrientations.join(', '));
}
}
}
the trace statement prints the full,
default, rotatedLeft, rotatedRight, upsideDown
But no matter how I turn, I only get default (upright portrait), rotatedLeft and rotatedRight, never upsideDown.
I'm using AIR 4.0, and have tested it on a Nexus 5, Samsung S2 and S3, and an iPhone 4. All works as expected on iOS.
If I manually handle it with,
stage.addEventListener(StageOrientationEvent.ORIENTATION_CHANGE, onReorientation);
...
protected function onReorientation(event:StageOrientationEvent):void
{
if(event.afterOrientation != stage.orientation)
{
stage.setOrientation(event.afterOrientation);
}
}
everything works as expected. When you go upside down, event.afterOrientation is upside down, but stage.orientation is still whatever it was before. Obviously this is a bug, no?
I have PreferenceFragment and a PreferenceActivity from which I add headers from a XML file on this way:
PreferenceActivity
#Override
public void onBuildHeaders(List<Header> target) {
if(DEBUG) Log.i("PreferenceActivity", "onBuildHeaders() -> LogicAnalizerPrefs");
if(android.os.Build.VERSION.SDK_INT >= 12) {
loadHeadersFromResource(R.xml.preference_header_logicanalizer, target);
}
}
PreferenceFragment:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(DEBUG) Log.i("PreferenceFragment", "onCreate() -> LogicAnalizerPrefsFragment");
int res = getActivity().getResources().getIdentifier(getArguments().getString("logicprefsheaders"), "xml", getActivity().getPackageName());
addPreferencesFromResource(res);
}
And the XML file where I have the headers is:
R.xml.preference_header_logicanalizer:
<header
android:fragment="com.protocolanalyzer.andres.LogicAnalizerPrefsFragment"
android:icon="#drawable/settings"
android:title="General" >
<extra
android:name="logicprefsheaders"
android:value="logicgeneral" />
</header>
<header
android:fragment="com.protocolanalyzer.andres.LogicAnalizerPrefsFragment"
android:icon="#drawable/settings"
android:title="Canal 1" >
<extra
android:name="logicprefsheaders"
android:value="c1analizerprefs" />
</header>
<header
android:fragment="com.protocolanalyzer.andres.LogicAnalizerPrefsFragment"
android:icon="#drawable/settings"
android:title="Canal 2" >
<extra
android:name="logicprefsheaders"
android:value="c2analizerprefs" />
</header>
<header
android:fragment="com.protocolanalyzer.andres.LogicAnalizerPrefsFragment"
android:icon="#drawable/settings"
android:title="Canal 3" >
<extra
android:name="logicprefsheaders"
android:value="c3analizerprefs" />
</header>
<header
android:fragment="com.protocolanalyzer.andres.LogicAnalizerPrefsFragment"
android:icon="#drawable/settings"
android:title="Canal 4" >
<extra
android:name="logicprefsheaders"
android:value="c4analizerprefs" />
</header>
And this is one of my xml files which is used to display one of the preferences when a Header is clicked:
c1analizerprefs.xml:
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
<PreferenceCategory android:title="#string/AnalyzerProtocolTitle1" >
<ListPreference
android:defaultValue="0"
android:entries="#array/protocolList"
android:entryValues="#array/protocolValues"
android:key="protocol1"
android:summary="#string/AnalyzerProtocolSummary"
android:title="#string/AnalyzerProtocolTitle1" />
<ListPreference
android:defaultValue="1"
android:entries="#array/channelNames"
android:entryValues="#array/protocolValues"
android:key="SCL1"
android:summary="#string/AnalyzerSCLSummary"
android:title="#string/AnalyzerSCLTitle" />
<EditTextPreference
android:defaultValue="9600"
android:title="#string/AnalyzerBaudTitle"
android:key="BaudRate1"
android:summary="#string/AnalyzerBaudSummary"
android:inputType="number" />
</PreferenceCategory>
</PreferenceScreen>
So in a large screen I have this result as expected and I use only one PreferenceFragment to add my 4 headers.
But my four Preferences defined in the XML like c1analizerprefs.xml are almost the same the only change is a number (For example: Pref. 1, Pref. 2, ...) so I want to add them in Java so I can use a for() to add 4 or more Preferences easily changing the number of repetitions because the text is always the same I only change a number so on this way I don't need to create one XML file for each preference, I create them dynamically in Java.
How can I add a Preference to a Header in Java instead of using a XML file? In PreferenceFragment I only have addPreferencesFromResource() or addPreferencesFromIntent(). Is any way to add a Preference Object?
Just in case someone is actually looking for some code...
The following example loads some headers from XML and then appends to them programatically.
1st override onBuildHeaders inside your PreferenceActivity:
#Override
public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.xml.preference_headers, target);
// Add layer as a new settings header
Header header = new Header();
header.title = "More Settings";
header.summary = "Change even more settings";
header.fragment = SettingsFragment.class.getName();
Bundle b = new Bundle();
b.putString("category", "MoreSettings");
header.fragmentArguments = b;
target.add(header);
}
Then override onHeaderClick inside your PreferenceActivity. Note that you pass the header's fragmentArguments to the fragment so you can detect which header was clicked (category):
#Override
public void onHeaderClick(Header header, int position) {
this.startPreferencePanel(SettingsFragment.class.getName(), header.fragmentArguments, header.titleRes, header.title, null, 0);
}
Then inside your PreferenceFragment override onCreate:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
mCategory = getArguments().getString("category");
}
else {
// Orientation Change
mCategory = savedInstanceState.getString("category");
}
if (mCategory.equals("Map")) {
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preferences_map);
}
if (mCategory.equals("MoreSettings")) {
// Load the preferences from an XML resource
PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(getActivity());
// add prefrences using preferenceScreen.addPreference()
this.setPreferenceScreen(preferenceScreen);
}
}
In case you want to see the xml for predefined headers.
res/xml/preference_headers.xml
<?xml version="1.0" encoding="utf-8"?>
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
<header
android:fragment="com.appName.fragments.SettingsFragment"
android:title="#string/pref_category_map_title"
android:summary="#string/pref_category_map_sum" >
<!-- key/value pairs can be included as arguments for the fragment. -->
<extra android:name="category" android:value="Map" />
</header>
</preference-headers>
res/xml/preferences_map.xml
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<CheckBoxPreference
android:key="pref_zoomToCurrentLocationOnStart"
android:title="#string/pref_zoomToCurrentLocationOnStart_title"
android:summary="#string/pref_zoomToCurrentLocationOnStart_sum"
android:defaultValue="true" />
<CheckBoxPreference
android:key="pref_myLocation"
android:title="#string/pref_myLocation_title"
android:summary="#string/pref_myLocation_sum"
android:defaultValue="true" />
</PreferenceScreen>
Adding headers dynamically will not be easy. As android documentation states:
Blockquote Typical implementations will use loadHeadersFromResource(int, List) to fill in the list from a resource.
If you still want to go with a dynamic solution, you might want to take a look at the source code of loadHeadersFromResource (for example, here http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.0.1_r1/android/preference/PreferenceActivity.java#PreferenceActivity.loadHeadersFromResource(int%2Cjava.util.List) )
To sum up,
you need to create a PreferenceActivity.Header object,
prepare it for usage by setting its various properties that can be found in documentation: http://developer.android.com/reference/android/preference/PreferenceActivity.Header.html,
and finally add that created header to the list: target.add(header); (target is the argument that is passed to onBuildHeaders)
Hopefully I haven't misunderstood your question:
getPreferenceScreen().addPreference( preference );
Great answer by #goodies4uall
I will merely add (since i cannot yet edit posts) that you need not do anything fancy in onHeaderClick().
If you wish to add a header outside of onBuildHeaders() the best way to do it is to invalidate them and allow onBuildHeaders() to add the new one when it next fires (invalidateHeaders will cause onBuildHeaders to fire again), like so:
private boolean flagToAddMyNewHeader = false;
public void someEventFiredThatRequiresANewHeader() {
flagToAddMyNewHeader = true;
invalidateHeaders();
}
And then your onBuildHeaders:
#Override
public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.xml.preference_headers, target);
if (flagToAddMyNewHeader) {
flagToAddMyNewHeader = false;
Header header = new Header();
header.titleRes = R.string.my_new_header_title;
header.iconRes = R.drawable.my_new_header_icon;
header.fragment = MyNewHeaderFragment.class.getName();
Bundle b = new Bundle();
b.putString("category", "MyNewHeader");
header.fragmentArguments = b;
// inserts my new header as the 2nd entry
target.add(1, header);
}
}
I've got this source xml:
<source>
<category id="1" />
<item1 />
<item2 />
<category id="2"/>
<item1 />
<item2 />
</source>
As you can see all items have the same hierarchy.
And I need to "translate"/serialize it to another XML like this:
<source>
<category id="1">
<item1 />
<item2 />
</category>
<category id="2">
<item1 />
<item2 />
</category>
</source>
Where "items" are children of "category".
I'm using XmlPullParser and XmlSerializer from Android tools but I don't mind to use another if they are compatible with Android environment
Tx
I've found another way using XSLT:
This way we use XML-only specific tools for transform without handling any data with objects.
create a transform.xsl file to handle transformation:
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" encoding="UTF-8" />
<xsl:strip-space elements="*" />
<xsl:template match="/">
<xsl:apply-templates />
</xsl:template>
<xsl:template match="source">
<source>
<xsl:apply-templates select="category" />
</source>
</xsl:template>
<xsl:template match="category">
<xsl:variable name="place" select="count(preceding-sibling::category)" />
<category>
<xsl:attribute name="id">
<xsl:value-of select="#id" />
</xsl:attribute>
<xsl:apply-templates select="following-sibling::*[not(self::category)]">
<xsl:with-param name="slot" select="$place" />
</xsl:apply-templates>
</category>
</xsl:template>
<xsl:template match="item1">
<xsl:param name="slot" />
<xsl:choose>
<xsl:when test="count(preceding-sibling::category) = $slot + 1">
<xsl:copy-of select="." />
</xsl:when>
<xsl:otherwise />
</xsl:choose>
</xsl:template>
<xsl:template match="item2">
<xsl:param name="slot" />
<xsl:choose>
<xsl:when test="count(preceding-sibling::category) = $slot + 1">
<xsl:copy-of select="." />
</xsl:when>
<xsl:otherwise />
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
And then write the code to handle transformation to a file with desired output data.xml
AssetManager am = getAssets();
xml = am.open("source.xml");
xsl = am.open("transform.xsl");
Source xmlSource = new StreamSource(xml);
Source xsltSource = new StreamSource(xsl);
TransformerFactory transFact = TransformerFactory.newInstance();
Transformer trans = transFact.newTransformer(xsltSource);
File f = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/data.xml");
StreamResult result = new StreamResult(f);
trans.transform(xmlSource, result);
And its done.
more info here:
http://www.dpawson.co.uk/xsl/sect2/flatfile.html
For an easy xml format this shouldn't be that hard.
A possible way to read the first xml file with a sax parser would be:
public class MySaxHandler extends DefaultHandler {
private List<Category> items = new LinkedList<Category>();
private Category currentCategory;
public void startElement(String uri, String localName, String qName, Attributes attributes) {
if (localName.equals("category")) {
currentCategory = new Category(attributes.getValue("id"));
items.add(currentCategory);
}
if (localName.equals("item1") {
currentCategory.setItem1(new Item1(...));
}
if (localName.equals("item2") {
currentCategory.setItem2(new Item2(...));
}
}
}
For each <category> tag you create a new Category object. The following items will be added to the last category object. While reading the contents you create the hierarchy you need later (items are added to the appropriate category).
It should be easy to transform this code to use XmlPullParser instead of sax parser. I just used sax because I am more familiar with it.
When you finished reading the first file you need to write your hierarchy to a new file.
You can do this in a way like this:
StringBuilder b = new StringBuilder();
for (int i = 0; i < categories.size(); i++) {
b.append(categories.get(i).getXml());
}
// write content of b into file
getXml() for each category could look like:
public String getXml() {
StringBuilder b = new StringBuilder();
b.append("<category id=\"" + this.id + "\">");
for (int i = 0; i < items.size(); i++) {
b.append(items.get(i).getXml());
}
b.append("</category>");
return b.toString();
}
Each item creates its own xml in its getXml() method which could be
public String getXml() {
return "<item1 />";
}
in the easiest case.
Please note that building xml by hand is only suitable if you xml structure stays that simple. If the structure is becoming more complicated you should make use of some lightweight xml libraries that work on Android (like xstream).