6-10. Sharing Your SharedPreferences

Problem

You would like your application to provide the settings values it has stored in SharedPreferences to other applications of the system and even to allow those applications to modify those settings if they have permission to do so.

Solution

(API Level 1)

Create a ContentProvider to interface your application’s SharedPreferences to the rest of the system. The settings data will be delivered using a MatrixCursor, which is an implementation that can be used for data that does not reside in a database. The ContentProvider will be protected by separate permissions to read/write the data within so that only permitted applications will have access.

How It Works

To properly demonstrate the permissions aspect of this recipe, we need to create two separate applications: one that actually contains our preference data and one that wants to read and modify it through the ContentProvider interface. This is because Android does not enforce permissions on anything operating within the same application. Let’s start with the provider, shown in Listing 6-30.

Listing 6-30. ContentProvider for Application Settings

public class SettingsProvider extends ContentProvider {
    
    public static final Uri CONTENT_URI =
        Uri.parse("content://com.examples.sharepreferences.settingsprovider/settings");
    
    public static class Columns {
        public static final String _ID = Settings.NameValueTable._ID;
        public static final String NAME = Settings.NameValueTable.NAME;
        public static final String VALUE = Settings.NameValueTable.VALUE;
    }
    
    private static final String NAME_SELECTION = Columns.NAME + " = ?";
    
    private SharedPreferences mPreferences;
 
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException(
                "This ContentProvider does not support removing Preferences");
    }
    
    @Override
    public String getType(Uri uri) {
        return null;
    }
 
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        throw new UnsupportedOperationException(
                "This ContentProvider does not support adding new Preferences");
    }
    
    @Override
    public boolean onCreate() {
        mPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
        return true;
    }
 
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        MatrixCursor cursor = new MatrixCursor(projection);
        Map<String, ?> preferences = mPreferences.getAll();
        Set<String> preferenceKeys = preferences.keySet();
        
        if(TextUtils.isEmpty(selection)) {
            //Get all items
            for(String key : preferenceKeys) {
                //Insert only the columns they requested
                MatrixCursor.RowBuilder builder = cursor.newRow();
                for(String column : projection) {
                    if(column.equals(Columns._ID)) {
                        //Generate a unique id
                        builder.add(key.hashCode());
                    }
                    if(column.equals(Columns.NAME)) {
                        builder.add(key);
                    }
                    if(column.equals(Columns.VALUE)) {
                        builder.add(preferences.get(key));
                    }
                }
            }
        } else if (selection.equals(NAME_SELECTION)) {
            //Parse the key value and check if it exists
            String key = selectionArgs == null ? "" : selectionArgs[0];
            if(preferences.containsKey(key)) {
                //Get the requested item
                MatrixCursor.RowBuilder builder = cursor.newRow();
                for(String column : projection) {
                    if(column.equals(Columns._ID)) {
                        //Generate a unique id
                        builder.add(key.hashCode());
                    }
                    if(column.equals(Columns.NAME)) {
                        builder.add(key);
                    }
                    if(column.equals(Columns.VALUE)) {
                        builder.add(preferences.get(key));
                    }
                }
            }
        }
        
        return cursor;
    }
 
    @Override
    public int update(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        //Check if the key exists, and update its value
        String key = values.getAsString(Columns.NAME);
        if (mPreferences.contains(key)) {
            Object value = values.get(Columns.VALUE);
            SharedPreferences.Editor editor = mPreferences.edit();
            if (value instanceof Boolean) {
                editor.putBoolean(key, (Boolean)value);
            } else if (value instanceof Number) {
                editor.putFloat(key, ((Number)value).floatValue());
            } else if (value instanceof String) {
                editor.putString(key, (String)value);
            } else {
                //Invalid value, do not update
              return 0;
            }
            editor.commit();
            //Notify any observers
            getContext().getContentResolver().notifyChange(CONTENT_URI, null);
            return 1;
        }
        //Key not in preferences
        return 0;
    }
}

Upon creation of this ContentProvider, we obtain a reference to the application’s default SharedPreferences rather than opening up a database connection as in the previous example. We support only two methods in this provider—query() and update()—and throw exceptions for the rest. This allows read/write access to the preference values without allowing any ability to add or remove new preference types.

Inside the query() method, we check the selection string to determine whether we should return all preference values or just the requested value. There are three fields defined for each preference: _id, name, and value. The value of _id may not be related to the preference itself, but if the client of this provider wants to display the results in a list by using CursorAdapter, this field will need to exist and have a unique value for each record, so we generate one. Notice that we obtain the preference value as an Object to insert in the cursor; we want to minimize the amount of knowledge the provider should have about the types of data it contains.

The cursor implementation used in this provider is a MatrixCursor, which is a cursor designed to be built around data not held inside a database. The example iterates through the list of columns requested (the projection) and builds each row according to these columns it contains. Each row is created by calling MatrixCursor.newRow(), which also returns a Builder instance that will be used to add the column data. Care should always be taken to match the order of the column data that is added to the order of the requested projection. They should always match.

The implementation of update() inspects only the incoming ContentValues for the preference it needs to update. Because this is enough to describe the exact item we need, we don’t implement any further logic using the selection arguments. If the name value of the preference already exists, the value for it is updated and saved. Unfortunately, there is no method to simply insert an Object back into SharedPreferences, so you must inspect it based on the valid types that ContentValues can return and call the appropriate setter method to match. Finally, we call notifyObservers() so any registered ContentObserver objects will be notified of the data change.

You may have noticed that there is no code in the ContentProvider to manage the read/write permissions we promised to implement! This is actually handled by Android for us: we just need to update the manifest appropriately. Have a look at Listing 6-31.

Listing 6-31. AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.examples.sharepreferences"
    android:versionCode="1"
    android:versionName="1.0" >
 
    <uses-sdk ... />
 
    <permission
        android:name="com.examples.sharepreferences.permission.READ_PREFERENCES"
        android:label="Read Application Settings"
        android:protectionLevel="normal" />
    <permission
        android:name="com.examples.sharepreferences.permission.WRITE_PREFERENCES"
        android:label="Write Application Settings"
        android:protectionLevel="dangerous" />
 
    <uses-permission
        android:name="com.examples.sharepreferences.permission.READ_PREFERENCES" />
    <uses-permission
        android:name="com.examples.sharepreferences.permission.WRITE_PREFERENCES" />
    
    <application ... >
        <activity android:name=".SettingsActivity" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="com.examples.sharepreferences.ACTION_SETTINGS" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>
 
        <provider
            android:name=".SettingsProvider"
            android:authorities="com.examples.sharepreferences.settingsprovider"
            android:readPermission=
                    "com.examples.sharepreferences.permission.READ_PREFERENCES"
            android:writePermission=
                    "com.examples.sharepreferences.permission.WRITE_PREFERENCES" >
        </provider>
    </application>
 
</manifest>

Here you can see two custom <permission> elements declared and attached to our <provider> declaration. This is the only code we need to add, and Android knows to enforce the read permissions for operations such as query(), and the write permission for insert(), update(), and delete(). We have also declared a custom <intent-filter> on the activity in this application, which will come in handy for any external applications that may want to launch the settings UI directly. Listings 6-32 through 6-34 define the rest of this example.

Listing 6-32. res/xml/preferences.xml

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
    <CheckBoxPreference
        android:key="preferenceEnabled"
        android:title="Set Enabled"
        android:defaultValue="true"/>
    <EditTextPreference
        android:key="preferenceName"
        android:title="User Name"
        android:defaultValue="John Doe"/>
    <ListPreference
        android:key="preferenceSelection"
        android:title="Selection"
        android:entries="@array/selection_items"
        android:entryValues="@array/selection_items"
        android:defaultValue="Four"/>
</PreferenceScreen>

Listing 6-33. res/values/arrays.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="selection_items">
        <item>One</item>
        <item>Two</item>
        <item>Three</item>
        <item>Four</item>
    </string-array>
</resources>

Listing 6-34. Preferences Activity

//Note the package for this application
package com.examples.sharepreferences;
 
public class SettingsActivity extends PreferenceActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //Load the preferences defaults on first run
        PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
        
        addPreferencesFromResource(R.xml.preferences);
    }
}

The settings values for this example application are manageable directly via a simple PreferenceActivity, whose data are defined in the preferences.xml file.

Note  PreferenceActivity was deprecated in Android 3.0 in favor of PreferenceFragment, but at the time of this book’s publication, PreferenceFragment has not yet been added to the Support Library. Therefore, we use it here to allow support for earlier versions of Android.

Usage Example

Next let’s take a look at Listings 6-35 through 6-37, which define a second application that will attempt to access our preferences data by using this ContentProvider interface.

Listing 6-35. AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.examples.accesspreferences"
    android:versionCode="1"
    android:versionName="1.0">
 
    <uses-sdk ... />
 
    <uses-permission
        android:name="com.examples.sharepreferences.permission.READ_PREFERENCES" />
    <uses-permission
        android:name="com.examples.sharepreferences.permission.WRITE_PREFERENCES" />
    
    <application ... >
        <activity android:name=".MainActivity" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
 
</manifest>

The key point here is that this application declares the use of both our custom permissions as <uses-permission> elements. This is what allows it to have access to the external provider. Without these, a request through ContentResolver would result in a SecurityException.

Listing 6-36. res/layout/main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <Button
        android:id="@+id/button_settings"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Show Settings"
        android:onClick="onSettingsClick" />
    <CheckBox
        android:id="@+id/checkbox_enable"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/button_settings"
        android:text="Set Enable Setting"/>
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:orientation="vertical">
        <TextView
            android:id="@+id/value_enabled"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <TextView
            android:id="@+id/value_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <TextView
            android:id="@+id/value_selection"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </LinearLayout>
</RelativeLayout>

Listing 6-37. Activity Interacting with the Provider

//Note the package as this is a different application
package com.examples.accesspreferences;
 
public class MainActivity extends Activity implements OnCheckedChangeListener {
    
    public static final String SETTINGS_ACTION =
        "com.examples.sharepreferences.ACTION_SETTINGS";
    public static final Uri SETTINGS_CONTENT_URI =
        Uri.parse("content://com.examples.sharepreferences.settingsprovider/settings");
    public static class SettingsColumns {
        public static final String _ID = Settings.NameValueTable._ID;
        public static final String NAME = Settings.NameValueTable.NAME;
        public static final String VALUE = Settings.NameValueTable.VALUE;
    }
    
    TextView mEnabled, mName, mSelection;
    CheckBox mToggle;
    
    private ContentObserver mObserver = new ContentObserver(new Handler()) {
        public void onChange(boolean selfChange) {
            updatePreferences();
        }
    };
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        mEnabled = (TextView) findViewById(R.id.value_enabled);
        mName = (TextView) findViewById(R.id.value_name);
        mSelection = (TextView) findViewById(R.id.value_selection);
        mToggle = (CheckBox) findViewById(R.id.checkbox_enable);
        mToggle.setOnCheckedChangeListener(this);
    }
 
    @Override
    protected void onResume() {
        super.onResume();
        //Get the latest provider data
        updatePreferences();
        //Register an observer for changes that will
        // happen while we are active
        getContentResolver().registerContentObserver(SETTINGS_CONTENT_URI,
                false, mObserver);
    }
    
    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        ContentValues cv = new ContentValues(2);
        cv.put(SettingsColumns.NAME, "preferenceEnabled");
        cv.put(SettingsColumns.VALUE, isChecked);
        
        //Update the provider, which will trigger our observer
        getContentResolver().update(SETTINGS_CONTENT_URI, cv, null, null);
    }
    
    public void onSettingsClick(View v) {
        try {
            Intent intent = new Intent(SETTINGS_ACTION);
            startActivity(intent);
        } catch (ActivityNotFoundException e) {
            Toast.makeText(this,
                    "You do not have the Android Recipes Settings App installed.",
                    Toast.LENGTH_SHORT).show();
        }
    }
    
    private void updatePreferences() {
        Cursor c = getContentResolver().query(SETTINGS_CONTENT_URI,
                new String[] {SettingsColumns.NAME, SettingsColumns.VALUE},
                null, null, null);
        if (c == null) {
            return;
        }
        
        while (c.moveToNext()) {
            String key = c.getString(0);
            if ("preferenceEnabled".equals(key)) {
                mEnabled.setText( String.format("Enabled Setting = %s",
                    c.getString(1)) );
                mToggle.setChecked( Boolean.parseBoolean(c.getString(1)) );
            } else if ("preferenceName".equals(key)) {
                mName.setText( String.format("User Name Setting = %s",
                    c.getString(1)) );
            } else if ("preferenceSelection".equals(key)) {
                mSelection.setText( String.format("Selection Setting = %s",
                    c.getString(1)) );
            }
        }
 
        c.close();
    }
}

Because this is a separate application, it may not have access to the constants defined in the first (unless you control both applications and use a library project or some other method), so we have redefined them here for this example. If you were producing an application with an external provider you would like other developers to use, it would be prudent to also provide a JAR library that contains the constants necessary to access the Uri and column data in the provider, similar to the API provided by ContactsContract and CalendarContract.

In this example, the activity queries the provider for the current values of the settings each time it returns to the foreground and displays them in a TextView. The results are returned in a Cursor with two values in each row: the preference name and its value. The activity also registers a ContentObserver so that if the values change while this activity is active, the displayed values can be updated as well. When the user changes the value of the CheckBox onscreen, this calls the provider’s update() method, which will trigger this observer to update the display.

Finally, if desired, the user could launch the SettingsActivity from the external application directly by clicking the Show Settings button. This calls startActivity() with an Intent containing the custom action string for which SettingsActivity is set to filter.

6-11. Sharing Your Other Data

Problem

You would like your application to provide the files or other private data it maintains to applications on the device.

Solution

(API Level 3)

Create a ContentProvider to act as an external interface for your application’s data. ContentProvider exposes an arbitrary set of data to external requests through a database-like interface of query(), insert(), update(), and delete(), though the implementation is free to design how the data passes to the actual model from these methods.

ContentProvider can be used to expose any type of application data, including the application’s resources and assets, to external requests.

How It Works

Let’s take a look at a ContentProvider implementation that exposes two data sources: an array of strings located in memory, and a series of image files stored in the application’s assets directory. As before, we must declare our provider to the Android system by using a <provider> tag in the manifest. See Listings 6-38 and 6-39.

Listing 6-38. Manifest Declaration for ContentProvider

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...>
    <application ...>
      <provider android:name=".ImageProvider"
          android:authorities="com.examples.share.imageprovider">
      </provider>
    </application>
</manifest>

Listing 6-39. Custom ContentProvider Exposing Assets

public class ImageProvider extends ContentProvider {
 
    public static final Uri CONTENT_URI =         Uri.parse("content://com.examples.share.imageprovider");
    
    public static final String COLUMN_NAME = "nameString";
    public static final String COLUMN_IMAGE = "imageUri";
    
    private String[] mNames;
    
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException("This ContentProvider is read-only");
    }
 
    @Override
    public String getType(Uri uri) {
        return null;
    }
 
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        throw new UnsupportedOperationException("This ContentProvider is read-only");
    }
 
    @Override
    public boolean onCreate() {
        mNames = new String[] {"John Doe", "Jane Doe", "Jill Doe"};
        return true;
    }
 
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
        String[] selectionArgs, String sortOrder) {
        MatrixCursor cursor = new MatrixCursor(projection);
        for(int i = 0; i < mNames.length; i++) {
            //Insert only the columns they requested
            MatrixCursor.RowBuilder builder = cursor.newRow();
            for(String column : projection) {
                if(column.equals("_id")) {
                    //Use the array index as a unique id
                    builder.add(i);
                }
                if(column.equals(COLUMN_NAME)) {
                    builder.add(mNames[i]);
                }
                if(column.equals(COLUMN_IMAGE)) {
                    builder.add(Uri.withAppendedPath(CONTENT_URI, String.valueOf(i)));
                }
            }
        }
        return cursor;
    }
 
    @Override
    public int update(Uri uri, ContentValues values, String selection,
        String[] selectionArgs) {
        throw new UnsupportedOperationException("This ContentProvider is read-only");
    }
    
    @Override
    public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws
        FileNotFoundException {
        int requested = Integer.parseInt(uri.getLastPathSegment());
        AssetFileDescriptor afd;
        AssetManager manager = getContext().getAssets();
        //Return the appropriate asset for the requested item
        try {
            switch(requested) {
            case 0:
                afd = manager.openFd("logo1.png");
                break;
            case 1:
                afd = manager.openFd("logo2.png");
                break;
            case 2:
                afd = manager.openFd("logo3.png");
                break;
            default:
                afd = manager.openFd("logo1.png");
            }
            return afd;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}

As you may have guessed, the example exposes three logo image assets. The images we have chosen for this example are shown in Figure 6-4.

9781430263227_Fig06-04.jpg

Figure 6-4. Examples of logo1.png (left), logo2.png (center), and logo3.png (right) stored in assets

Because we are exposing read-only content in the assets directory, there is no need to support the inherited methods insert(), update(), or delete(), so we have these methods simply throw an UnsupportedOperationException.

When the provider is created, the string array that holds people’s names is created and onCreate() returns true; this signals to the system that the provider was created successfully. The provider exposes constants for its Uri and all readable column names. These values will be used by external applications to make requests for data.

This provider supports only a query for all the data within it. To support conditional queries for specific records or a subset of all the content, an application can process the values passed in to query() for selection and selectionArgs. In this example, any call to query() will build a cursor with all three elements contained within.

The cursor implementation used in this provider is a MatrixCursor, which is a cursor designed to be built around data that is not held inside a database. The example iterates through the list of columns requested (the projection) and builds each row according to these columns it contains. Each row is created by calling MatrixCursor.newRow(), which also returns a Builder instance that will be used to add the column data. Care should always be taken to match the order that the column data is added to the order of the requested projection. They should always match.

The value in the name column is the respective string in the local array, and the _id value, which Android requires to utilize the returned cursor with most ListAdapters, is simply returned as the array index. The information presented in the image column for each row is actually a content Uri representing the image file for each row, created with the provider’s content Uri as the base, with the array index appended to it.

When an external application actually goes to retrieve this content, through ContentResolver.openInputStream(), a call will be made to openAssetFile(), which has been overridden to return an AssetFileDescriptor pointing to one of the image files in the assets directory. This implementation determines which image file to return by deconstructing the content Uri once again and retrieving the appended index value from the end.

Usage Example

Let’s take a look at how this provider should be implemented and accessed in the context of the Android application. See Listing 6-40.

Listing 6-40. AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.examples.share"
    android:versionCode="1"
    android:versionName="1.0">
    <uses-sdk android:minSdkVersion="3" />
 
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".ShareActivity"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <provider android:name=".ImageProvider"
          android:authorities="com.examples.share.imageprovider">
        </provider>
    </application>
</manifest>

To implement this provider, the manifest of the application that owns the content must declare a <provider> tag pointing out the ContentProvider name and the authority to match when requests are made. The authority value should match the base portion of the exposed content Uri. The provider must be declared in the manifest so the system can instantiate and run it, even when the owning application is not running. See Listings 6-41 and 6-42.

Listing 6-41. res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="match_parent"
  android:layout_height="match_parent">
  <TextView
    android:id="@+id/name"
    android:layout_width="wrap_content"
    android:layout_height="20dip"
    android:layout_gravity="center_horizontal"
  />
  <ImageView
    android:id="@+id/image"
    android:layout_width="wrap_content"
    android:layout_height="50dip"
    android:layout_gravity="center_horizontal"
  />
  <ListView
    android:id="@+id/list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
  />
</LinearLayout>

Listing 6-42. Activity Reading from ImageProvider

public class ShareActivity extends FragmentActivity implements
        LoaderManager.LoaderCallbacks<Cursor>, AdapterView.OnItemClickListener {
    private static final int LOADER_LIST = 100;
    SimpleCursorAdapter mAdapter;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getSupportLoaderManager().initLoader(LOADER_LIST, null, this);
        setContentView(R.layout.main);
 
        mAdapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_1,
                null, new String[]{ImageProvider.COLUMN_NAME},
                new int[]{android.R.id.text1}, 0);
        
        ListView list = (ListView)findViewById(R.id.list);
        list.setOnItemClickListener(this);
        list.setAdapter(mAdapter);
    }
 
    @Override
    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
        //Seek the cursor to the selection
        Cursor c = mAdapter.getCursor();
        c.moveToPosition(position);
        
        //Load the name column into the TextView
        TextView tv = (TextView)findViewById(R.id.name);
        tv.setText(c.getString(1));
        
        ImageView iv = (ImageView)findViewById(R.id.image);
        try {
            //Load the content from the image column into the ImageView
            InputStream in =
                    getContentResolver().openInputStream(Uri.parse(c.getString(2)));
            Bitmap image = BitmapFactory.decodeStream(in);
            iv.setImageBitmap(image);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
    
    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        String[] projection = new String[]{"_id",
                ImageProvider.COLUMN_NAME,
                ImageProvider.COLUMN_IMAGE};
        return new CursorLoader(this, ImageProvider.CONTENT_URI,
                projection, null, null, null);
    }
 
    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        mAdapter.swapCursor(data);
    }
 
    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        mAdapter.swapCursor(null);
    }
}

Important  This example requires the Support Library to provide access to the Loader pattern in Android 1.6 and above. If you are targeting Android 3.0+ in your application, you may replace FragmentActivity with Activity and getSupportLoaderManager() with getLoaderManager().

In this example, a managed cursor is obtained from the custom ContentProvider, referencing the exposed Uri and column names for the data. The data is then connected to a ListView through a SimpleCursorAdapter to display only the name value.

When the user taps any of the items in the list, the cursor is moved to that position and the respective name and image are displayed above. This is where the activity calls ContentResolver.openInputStream() to access the asset images through the Uri that was stored in the column field.

Figure 6-5 displays the result of running this application and selecting the last item in the list (Jill Doe).

9781430263227_Fig06-05.jpg

Figure 6-5. Activity drawing resources from ContentProvider

Note that we have not closed the connection to the Cursor explicitly. Since the Loader created the Cursor, it is also the job of the Loader to manage its life cycle.

DocumentsProvider

(API Level 19)

The DocumentsProvider is a specialized ContentProvider API that applications can use to expose their contents to the common documents picker interface in Android 4.4 and later. The advantage to using this framework is that it allows applications that manage access to storage services to expose the files and documents they own by using a common interface throughout the system. It also includes the ability for client applications to create and save new documents inside these applications (we will look more at the client side of this API in the next chapter).

A custom DocumentsProvider must identify all the files and directories it would like to expose by using a unique document ID string. This value does not need to match any specific format, but it must be unique and cannot change after it is reported to the system. The framework will persist these values for permissions purposes, so even across reboots the document IDs you provide (and later expect) must be consistent for any resource.

When subclassing DocumentsProvider, we will be implementing a different set of callbacks than the basic CRUD methods we used in the bare ContentProvider. The system’s document picker interface will trigger the following methods on the provider as the user explores:

  • queryRoots(): First called when the picker UI is launched to request basic information about the top-level “document” in your provider, as well as some basic metadata such as the name and icon to display. Most providers have only one root, but multiple roots can be returned if that better supports the provider’s use case.
  • queryChildDocuments(): Called when the provider is selected with the root’s document ID in order to get a listing of the documents available under this root. If you return directory entries as elements underneath the root, the same method will be called again when one of those subdirectories is selected.
  • queryDocument(): Called when a document is selected to obtain metadata about that specific instance. The data returned from this message should mirror what was returned from queryChildDocuments(), but for just the one element. This method is also called for each root to obtain additional metadata of the top-level directory in the provider.
  • openDocument(): This is a request to open a FileDescriptor to the document so that the client application may read or write the document contents.
  • openDocumentThumbnail(): If the metadata returned for a given document has the FLAG_SUPPORTS_THUMBNAIL flag set, this method is used to obtain a thumbnail to display in the picker UI for the document.

Listing 6-43 shows our ImageProvider modified to subclass DocumentProvider instead.

Listing 6-43. ImageProvider as a DocumentsProvider

public class ImageProvider extends DocumentsProvider {
    private static final String TAG = "ImageProvider";
    
    /* Cached recent selection */
    private static String sLastFilename;
    private static String sLastTitle;
    
    /* Default projection for a root when none supplied */
    private static final String[] DEFAULT_ROOT_PROJECTION = {
        Root.COLUMN_ROOT_ID, Root.COLUMN_MIME_TYPES,
        Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
        Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID,
        Root.COLUMN_AVAILABLE_BYTES
    };
    /* Default projection for documents when none supplied */
    private static final String[] DEFAULT_DOCUMENT_PROJECTION = {
        Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
        Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
        Document.COLUMN_FLAGS, Document.COLUMN_SIZE
    };
    
    private ArrayMap<String, String> mDocuments;
    
    @Override
    public boolean onCreate() {
        //Dummy data for our documents
        mDocuments = new ArrayMap<String, String>();
        mDocuments.put("logo1.png", "John Doe");
        mDocuments.put("logo2.png", "Jane Doe");
        mDocuments.put("logo3.png", "Jill Doe");
 
        //Dump asset images onto internal storage
        writeAssets(mDocuments.keySet());
        return true;
    }
    
    /*
     * Helper method to stream some dummy files out to the
     * internal storage directory
     */
    private void writeAssets(Set<String> filenames) {
        for(String name : filenames) {
            try {
                Log.d("ImageProvider", "Writing "+name+" to storage");
                InputStream in = getContext().getAssets().open(name);
                FileOutputStream out = getContext().openFileOutput(name, Context.MODE_PRIVATE);
 
                int size;
                byte[] buffer = new byte[1024];
                while ((size = in.read(buffer, 0, 1024)) >= 0) {
                    out.write(buffer, 0, size);
                }
                out.flush();
                out.close();
            } catch (IOException e) {
                Log.w(TAG, e);
            }
        }
    }
    
    /* Helper method to construct documentId from a file name */
    private String getDocumentId(String filename) {
        return "root:" + filename;
    }
    
    /*
     * Helper method to extract file name from a documentId.
     * Returns empty string for the "root" document.
     */
    private String getFilename(String documentId) {
        int split = documentId.indexOf(":");
        if (split < 0) {
            return "";
        }
        return documentId.substring(split+1);
    }
    
    /*
     * Called by the system to determine how many "providers" are
     * hosted here.  It is most common to return only one, via a
     * Cursor that has only one result row.
     */
    @Override
    public Cursor queryRoots(String[] projection) throws FileNotFoundException {
        if (projection == null) {
            projection = DEFAULT_ROOT_PROJECTION;
        }
        MatrixCursor result = new MatrixCursor(projection);
        //Add the single root for this provider
        MatrixCursor.RowBuilder builder = result.newRow();
        
        builder.add(Root.COLUMN_ROOT_ID, "root");
        builder.add(Root.COLUMN_TITLE, "Android Recipes");
        builder.add(Root.COLUMN_SUMMARY, "Android Recipes Documents Provider");
        builder.add(Root.COLUMN_ICON, R.drawable.ic_launcher);
        
        builder.add(Root.COLUMN_DOCUMENT_ID, "root:");
        
        builder.add(Root.COLUMN_FLAGS,
                //Results will come from only the local filesystem
                Root.FLAG_LOCAL_ONLY
                //We support showing recently selected items
                | Root.FLAG_SUPPORTS_RECENTS);
        builder.add(Root.COLUMN_MIME_TYPES, "image/*");
        builder.add(Root.COLUMN_AVAILABLE_BYTES, 0);
        
        return result;
    }
 
    /*
     * Called by the system to determine the child items for a given
     * parent.  Will be called for the root, and for each subdirectory
     * defined within.
     */
    @Override
    public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
            String sortOrder) throws FileNotFoundException {
 
        if (projection == null) {
            projection = DEFAULT_DOCUMENT_PROJECTION;
        }
        MatrixCursor result = new MatrixCursor(projection);
        
        try {
            for(String key : mDocuments.keySet()) {
                addImageRow(result, mDocuments.get(key), key);
            }
        } catch (IOException e) {
            return null;
        }
        
        return result;
    }
    
    /*
     * Return the same information provided via queryChildDocuments(), but
     * just for the single documentId requested.
     */
    @Override
    public Cursor queryDocument(String documentId, String[] projection)
            throws FileNotFoundException {
 
        if (projection == null) {
            projection = DEFAULT_DOCUMENT_PROJECTION;
        }
        
        MatrixCursor result = new MatrixCursor(projection);
        
        try {
            String filename = getFilename(documentId);
            if (TextUtils.isEmpty(filename)) {
                //This is a query for root
                addRootRow(result);
            } else {
                addImageRow(result, mDocuments.get(filename), filename);
            }
        } catch (IOException e) {
            return null;
        }
        
        return result;
    }
    
    /*
     * Called to populate any recently used items from this
     * provider in the Recents picker UI.
     */
    @Override
    public Cursor queryRecentDocuments(String rootId, String[] projection)
            throws FileNotFoundException {
 
        if (projection == null) {
            projection = DEFAULT_DOCUMENT_PROJECTION;
        }
        
        MatrixCursor result = new MatrixCursor(projection);
        
        if (sLastFilename != null) {
            try {
                addImageRow(result, sLastTitle, sLastFilename);
            } catch (IOException e) {
                Log.w(TAG, e);
            }
        }
        Log.d(TAG, "Recents: "+result.getCount());
        //We'll return the last selected result to a recents query
        return result;
    }
    
    /*
     * Helper method to write the root into the supplied
     * Cursor
     */
    private void addRootRow(MatrixCursor cursor) {
        final MatrixCursor.RowBuilder row = cursor.newRow();
        
        row.add(Document.COLUMN_DOCUMENT_ID, "root:");
        row.add(Document.COLUMN_DISPLAY_NAME, "Root");
        row.add(Document.COLUMN_SIZE, 0);
        row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
        
        long installed;
        try {
            installed = getContext().getPackageManager()
                    .getPackageInfo(getContext().getPackageName(), 0)
                    .firstInstallTime;
        } catch (NameNotFoundException e) {
            installed = 0;
        }
        row.add(Document.COLUMN_LAST_MODIFIED, installed);
        row.add(Document.COLUMN_FLAGS, 0);
    }
    
    /*
     * Helper method to write a specific image file into
     * the supplied Cursor
     */
    private void addImageRow(MatrixCursor cursor, String title, String filename)
            throws IOException {
 
        final MatrixCursor.RowBuilder row = cursor.newRow();
        
        AssetFileDescriptor afd = getContext().getAssets().openFd(filename);
        
        row.add(Document.COLUMN_DOCUMENT_ID, getDocumentId(filename));
        row.add(Document.COLUMN_DISPLAY_NAME, title);
        row.add(Document.COLUMN_SIZE, afd.getLength());
        row.add(Document.COLUMN_MIME_TYPE, "image/*");
        
        long installed;
        try {
            installed = getContext().getPackageManager()
                    .getPackageInfo(getContext().getPackageName(), 0)
                    .firstInstallTime;
        } catch (NameNotFoundException e) {
            installed = 0;
        }
        row.add(Document.COLUMN_LAST_MODIFIED, installed);
        row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_THUMBNAIL);
    }
 
    /*
     * Return a reference to an image asset the framework will use
     * in the items list for any document with the FLAG_SUPPORTS_THUMBNAIL
     * flag enabled.  This method is safe to block while downloading content.
     */
    @Override
    public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint,
            CancellationSignal signal) throws FileNotFoundException {
 
        //We will load the thumbnail from the version on storage
        String filename = getFilename(documentId);
        //Create a file reference to the image on internal storage
        final File file = new File(getContext().getFilesDir(), filename);
        //Return a file descriptor wrapping the file reference
        final ParcelFileDescriptor pfd =
                ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
        return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
    }
    
    /*
     * Return a file descriptor to the document referenced by the supplied
     * documentId.  The client will use this descriptor to read the contents
     * directly.  This method is safe to block while downloading content.
     */
    @Override
    public ParcelFileDescriptor openDocument(String documentId, String mode,
            CancellationSignal signal) throws FileNotFoundException {
 
        //We will load the document itself from assets
        try {
            String filename = getFilename(documentId);
            //Return the file descriptor directly from APK assets
            AssetFileDescriptor afd = getContext().getAssets().openFd(filename);
            
            //Save this as the last selected document
            sLastFilename = filename;
            sLastTitle = mDocuments.get(filename);
 
            return afd.getParcelFileDescriptor();
        } catch (IOException e) {
            Log.w(TAG, e);
            return null;
        }
    }
    
    /*
     * This method is invoked when openDocument() receives a file descriptor
     * from assets.  Documents opened from standard files will not invoke
     * this method.
     */
    @Override
    public AssetFileDescriptor openAssetFile(Uri uri, String mode)
            throws FileNotFoundException {
 
        //Last segment of Uri is the documentId
        String filename = getFilename(uri.getLastPathSegment());
        
        AssetManager manager = getContext().getAssets();
        try {
            //Return the appropriate asset for the requested item
            AssetFileDescriptor afd = manager.openFd(filename);
            
            //Save this as the last selected document
            sLastFilename = filename;
            sLastTitle = mDocuments.get(filename);
            
            return afd;
        } catch (IOException e) {
            Log.w(TAG, e);
            return null;
        }
    }
}

In this example, we show how to serve the same three logo image files used previously from the assets directory of our APK and from internal storage to indicate the method of opening a resource from both locations. In order to do this, when the provider is created, we read the image files out of assets and copy them to internal storage.

We have created a simple structure of converting file names into document IDs. In our case, root is the virtual top-level directory where our logo images live, and we create each ID as a pseudo-path to that file by using colon separators. The methods getDocumentId() and getFilename() are helpers to convert back and forth between our published ID and the actual image file name.

Tip  Document IDs will be embedded in a content Uri by the framework, so if you are converting directory paths to IDs, you must use characters that are not otherwise considered a path separator by the Uri class.

Inside queryRoots(), we return a MatrixCursor that includes the basic metadata of the single provider root. Notice that the query methods of the provider should respect the column projection passed in, and return only the data requested. We are using an updated version of the add() method as well that takes the column name for each item. This version is convenient, as it monitors the projection passed into the MatrixCursor constructor, and silently ignores attempts to add columns not in the projection, thus eliminating the looping we did before to add elements.

The title, summary, and icon columns deal with the provider display in the picker UI. We have also defined the following:

  • COLUMN_DOCUMENT_ID: Provides the ID we will be handed back later to reference this top-level root element.
  • COLUMN_MIME_TYPES: Reports the document types this root contains. We have image files, so we are using image/*.

In addition, COLUMN_FLAGSreports additional features the root item may support. The options are as follows:

  • FLAG_LOCAL_ONLY: Results are on the device, and don’t require network requests.
  • FLAG_SUPPORTS_CREATE: The root allows client applications to create a new document inside this provider. We will discuss how to do this on the client side in the next chapter.
  • FLAG_SUPPORTS_RECENTS: Tells the framework we can participate in the recent documents UI with results. This will result in calls to queryRecentDocuments() to obtain this metadata.
  • FLAG_SUPPORTS_SEARCH: Similar to recents, tells the framework we can handle search queries via querySearchDocuments().

We have set the local and recents flags in our example. Once this method returns, the framework will call queryDocument()with the document ID of the root to get more information. Inside addRootRow() we populate the cursor with the necessary fields. For COLUMN_MIME_TYPE we use the constant MIME_TYPE_DIR to indicate this element is a directory containing other documents. This same definition should be applied to any subdirectories in the hierarchy you create for the provider. Also, since all the files we are providing have existed since we installed the application, we provide the APK install date as the COLUMN_LAST_MODIFIEDvalue; for a more dynamic filesystem, this could just be the modified date of the file on disk.

When the provider is selected by the user, we receive a call to queryChildDocuments()to list all the files in the root. For us, this includes adding a row to the cursor for each logo image file we have. The addImageRow() method constructs the appropriate column data with similar elements to the previous iterations.

We want to allow each image to be represented by a thumbnail image in the picker UI, so we set the FLAG_SUPPORTS_THUMBNAILfor COLUMN_FLAGS on each image row. This will trigger openDocumentThumbnail() for each element as they are displayed in the picker. In this method, we’ve shown how to open a FileDescriptor from internal storage and return it. If this technique were used for openDocument(), the last step of wrapping the ParcelFileDescriptor in an AssetFileDescriptor would not be necessary.

The sizeHint parameter should be used to ensure you don’t return a thumbnail that is too large for display in the picker’s list. Our images are all small to begin with, so we haven’t checked this parameter here. It is safe, if necessary, to block and download content inside this method. For this case, a CancellationSignal is provided, which should be checked regularly in case the framework cancels the load before it is finished.

When a document is finally selected, queryDocument() will be called again with the ID of the logo image supplied. In this case, we must simply return the same results from addImageRow() for the single document requested. This will trigger a call to openDocument(), where we must return a valid ParcelFileDescriptor that the client can use to access the resource.

In our specific example, we return the file from assets directly. When the framework realizes we have returned a FileDescriptorfrom inside our APK, the secondary method openAssetFile() will be called to obtain the raw AssetFileDescriptor. In most cases where your content lives on other storage, this is not necessary. However, if you choose to expose files in assets, you must implement openAssetFile() as well, or the loading will fail.

The definition for our provider in the manifest also looks a bit different from before. Since the provider will be queried directly by the framework, we must define some specific filters and permissions (see Listing 6-44).

Listing 6-44. AndroidManifest.xml DocumentsProvider Snippet

<provider
    android:name="com.androidrecipes.sharedocuments.ImageProvider"
    android:authorities="com.androidrecipes.sharedocuments.images"
    android:grantUriPermissions="true"
    android:exported="true"
    android:permission="android.permission.MANAGE_DOCUMENTS">
    <!-- Unique filter the system will use to find published providers -->
    <intent-filter>
        <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
    </intent-filter>
</provider>

First, the provider must be exported so external applications can access it. This is usually the default behavior with a provider that has an <intent-filter> attached, but it’s good to be explicit here. The filter must include the DOCUMENTS_PROVIDER action, which is how the framework will find installed providers it can access. Next, the provider must be protected by the MANAGE_DOCUMENTS permission. This is a system-level permission that only system applications can obtain, so this protects your provider from being exploited by other apps. Finally, the grantUriPermissions attribute should be enabled. This allows the framework to provide access permissions to client applications on a document-by-document basis, rather than giving each client access to the whole provider.

This example application doesn’t really have a user interface to launch, but with the application installed, you can invoke the new provider by going to any system application that requires you to pick an image; the Contacts application is a good choice. When creating a new contact, you can add a photo, and selecting an existing image invokes the system picker UI. You can see in Figure 6-6 what this would look like for our example.

9781430263227_Fig06-06.jpg

Figure 6-6. Provider shown in list (left) with file options shown once selected (right)

Recent Documents

Remember in our example that we set the FLAG_SUPPORTS_RECENTS in the root metadata. We also provided an implementation of queryRecentDocuments() to react to these inquiries. There is no inherent limit to the number of recent documents any provider can return here, but you will want to pick something that is relevant and contextual to the user. Here, we return only the last selected logo image from our provider (something that we save on each open request in a static variable). The metadata here is the same as any other documents query, so the same addImageRow() method is invoked to populate the cursor.

With this in place, when we access the Recent section of our provider (as shown in Figure 6-7), we can see the last image selection made.

9781430263227_Fig06-07.jpg

Figure 6-7. Last select image shown in Recent UI

Summary

In this chapter, you investigated a number of practical methods to persist data on Android devices. You learned how to quickly create a preferences screen as well as how to use preferences and a simple method for persisting basic data types. You saw how and where to place files, for reference as well as storage. You even learned how to share your persisted data with other applications. In the next chapter, we will investigate how to leverage the operating system’s services to do background operations and to communicate between applications.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.131.152.166