Reputation: 111
As I understood from countless hours of trying to solve the issue, the activity which executes this method (RemoteViews.setImageViewUri()
) is the "home screen" activity, and it has no permissions for reading arbitrary files. But my application (widget) has them (), and therefore can read and display images from system gallery (in MainActivity or in "configuration screen" of the widget). And there is no Intent
to which you can give any permission flag (to allow the use of Uri
or storage read).
So is it possible to display images (from external storage) in the widget at all? I mean, the ImageView is allowed in RemoteViews
for something, right?
From https://developer.android.com/reference/android/widget/RemoteViews.html:
RemoteViews
is limited to support for the following layouts:
AdapterViewFlipper
FrameLayout
GridLayout
GridView
LinearLayout
ListView
RelativeLayout
StackView
ViewFlipper
And the following widgets:
AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextClock
TextView
As of API 31, the following widgets and layouts may also be used:
CheckBox
RadioButton
RadioGroup
Switch
Descendants of these classes are not supported.
Upvotes: 0
Views: 858
Reputation: 1495
setImageViewUri
method accepts a Uri. In general, when sharing to outside the app, the Uri should be generated by a FileProvider.
However, the exported
attribute of FileProvider is set to false by default. Prior to Android 12 and on certain Android 13 systems, the system process lacks permission to access an app's non-exported FileProvider.
Hence, you need an exportable FileProvider. Furthermore, it's important to note that an exported FileProvider permits any 3rd-party apps to access the directories exposed by this FileProvider. Therefore, you must be especially cautious in restricting the paths exposed to the outside. To illustrate exposing an exported
directory under internal cache storage, consider the following code:
ExportedFileProvider.java (Based on FileProvider.java from androidx)
I have made the following modifications:
You can directly copy this code segment in its entirety to serve as your ExportedFileProvider.java.
public class ExportedFileProvider extends ContentProvider {
private static final String[] COLUMNS = {
OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };
private static final String
META_DATA_FILE_PROVIDER_PATHS = "android.support.FILE_PROVIDER_PATHS";
private static final String TAG_ROOT_PATH = "root-path";
private static final String TAG_FILES_PATH = "files-path";
private static final String TAG_CACHE_PATH = "cache-path";
private static final String TAG_EXTERNAL = "external-path";
private static final String TAG_EXTERNAL_FILES = "external-files-path";
private static final String TAG_EXTERNAL_CACHE = "external-cache-path";
private static final String TAG_EXTERNAL_MEDIA = "external-media-path";
private static final String ATTR_NAME = "name";
private static final String ATTR_PATH = "path";
private static final String DISPLAYNAME_FIELD = "displayName";
private static final File DEVICE_ROOT = new File("/");
@GuardedBy("sCache")
private static final HashMap<String, PathStrategy> sCache = new HashMap<>();
private PathStrategy mStrategy;
private int mResourceId;
public ExportedFileProvider() {
mResourceId = ResourcesCompat.ID_NULL;
}
protected ExportedFileProvider(@XmlRes int resourceId) {
mResourceId = resourceId;
}
/**
* The default FileProvider implementation does not need to be initialized. If you want to
* override this method, you must provide your own subclass of FileProvider.
*/
@Override
public boolean onCreate() {
return true;
}
/**
* After the FileProvider is instantiated, this method is called to provide the system with
* information about the provider.
*
* @param context A {@link Context} for the current component.
* @param info A {@link ProviderInfo} for the new provider.
*/
@SuppressWarnings("StringSplitter")
@Override
public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
super.attachInfo(context, info);
// Check our security attributes
// if (info.exported) {
// throw new SecurityException("Provider must not be exported");
// }
if (!info.grantUriPermissions) {
throw new SecurityException("Provider must grant uri permissions");
}
String authority = info.authority.split(";")[0];
synchronized (sCache) {
sCache.remove(authority);
}
mStrategy = getPathStrategy(context, authority, mResourceId);
}
/**
* Return a content URI for a given {@link File}. Specific temporary
* permissions for the content URI can be set with
* {@link Context#grantUriPermission(String, Uri, int)}, or added
* to an {@link Intent} by calling {@link Intent#setData(Uri) setData()} and then
* {@link Intent#setFlags(int) setFlags()}; in both cases, the applicable flags are
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and
* {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. A FileProvider can only return a
* <code>content</code> {@link Uri} for file paths defined in their <code><paths></code>
* meta-data element. See the Class Overview for more information.
*
* @param context A {@link Context} for the current component.
* @param authority The authority of a {@link FileProvider} defined in a
* {@code <provider>} element in your app's manifest.
* @param file A {@link File} pointing to the filename for which you want a
* <code>content</code> {@link Uri}.
* @return A content URI for the file.
* @throws IllegalArgumentException When the given {@link File} is outside
* the paths supported by the provider.
*/
public static Uri getUriForFile(@NonNull Context context, @NonNull String authority,
@NonNull File file) {
final PathStrategy strategy = getPathStrategy(context, authority, ResourcesCompat.ID_NULL);
return strategy.getUriForFile(file);
}
/**
* Return a content URI for a given {@link File}. Specific temporary
* permissions for the content URI can be set with
* {@link Context#grantUriPermission(String, Uri, int)}, or added
* to an {@link Intent} by calling {@link Intent#setData(Uri) setData()} and then
* {@link Intent#setFlags(int) setFlags()}; in both cases, the applicable flags are
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and
* {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. A FileProvider can only return a
* <code>content</code> {@link Uri} for file paths defined in their <code><paths></code>
* meta-data element. See the Class Overview for more information.
*
* @param context A {@link Context} for the current component.
* @param authority The authority of a {@link FileProvider} defined in a
* {@code <provider>} element in your app's manifest.
* @param file A {@link File} pointing to the filename for which you want a
* <code>content</code> {@link Uri}.
* @param displayName The filename to be displayed. This can be used if the original filename
* is undesirable.
* @return A content URI for the file.
* @throws IllegalArgumentException When the given {@link File} is outside
* the paths supported by the provider.
*/
@SuppressLint("StreamFiles")
@NonNull
public static Uri getUriForFile(@NonNull Context context, @NonNull String authority,
@NonNull File file, @NonNull String displayName) {
Uri uri = getUriForFile(context, authority, file);
return uri.buildUpon().appendQueryParameter(DISPLAYNAME_FIELD, displayName).build();
}
/**
* Use a content URI returned by
* {@link #getUriForFile(Context, String, File) getUriForFile()} to get information about a file
* managed by the FileProvider.
* FileProvider reports the column names defined in {@link OpenableColumns}:
* <ul>
* <li>{@link OpenableColumns#DISPLAY_NAME}</li>
* <li>{@link OpenableColumns#SIZE}</li>
* </ul>
* For more information, see
* {@link ContentProvider#query(Uri, String[], String, String[], String)
* ContentProvider.query()}.
*
* @param uri A content URI returned by {@link #getUriForFile}.
* @param projection The list of columns to put into the {@link Cursor}. If null all columns are
* included.
* @param selection Selection criteria to apply. If null then all data that matches the content
* URI is returned.
* @param selectionArgs An array of {@link String}, containing arguments to bind to
* the <i>selection</i> parameter. The <i>query</i> method scans <i>selection</i> from left to
* right and iterates through <i>selectionArgs</i>, replacing the current "?" character in
* <i>selection</i> with the value at the current position in <i>selectionArgs</i>. The
* values are bound to <i>selection</i> as {@link String} values.
* @param sortOrder A {@link String} containing the column name(s) on which to sort
* the resulting {@link Cursor}.
* @return A {@link Cursor} containing the results of the query.
*
*/
@NonNull
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
@Nullable String[] selectionArgs,
@Nullable String sortOrder) {
// ContentProvider has already checked granted permissions
final File file = mStrategy.getFileForUri(uri);
String displayName = uri.getQueryParameter(DISPLAYNAME_FIELD);
if (projection == null) {
projection = COLUMNS;
}
String[] cols = new String[projection.length];
Object[] values = new Object[projection.length];
int i = 0;
for (String col : projection) {
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
cols[i] = OpenableColumns.DISPLAY_NAME;
values[i++] = (displayName == null) ? file.getName() : displayName;
} else if (OpenableColumns.SIZE.equals(col)) {
cols[i] = OpenableColumns.SIZE;
values[i++] = file.length();
}
}
cols = copyOf(cols, i);
values = copyOf(values, i);
final MatrixCursor cursor = new MatrixCursor(cols, 1);
cursor.addRow(values);
return cursor;
}
/**
* Returns the MIME type of a content URI returned by
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
*
* @param uri A content URI returned by
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
* @return If the associated file has an extension, the MIME type associated with that
* extension; otherwise <code>application/octet-stream</code>.
*/
@Nullable
@Override
public String getType(@NonNull Uri uri) {
// ContentProvider has already checked granted permissions
final File file = mStrategy.getFileForUri(uri);
final int lastDot = file.getName().lastIndexOf('.');
if (lastDot >= 0) {
final String extension = file.getName().substring(lastDot + 1);
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mime != null) {
return mime;
}
}
return "application/octet-stream";
}
/**
* By default, this method throws an {@link UnsupportedOperationException}. You must
* subclass FileProvider if you want to provide different functionality.
*/
@Override
public Uri insert(@NonNull Uri uri, @NonNull ContentValues values) {
throw new UnsupportedOperationException("No external inserts");
}
/**
* By default, this method throws an {@link UnsupportedOperationException}. You must
* subclass FileProvider if you want to provide different functionality.
*/
@Override
public int update(@NonNull Uri uri, @NonNull ContentValues values, @Nullable String selection,
@Nullable String[] selectionArgs) {
throw new UnsupportedOperationException("No external updates");
}
/**
* Deletes the file associated with the specified content URI, as
* returned by {@link #getUriForFile(Context, String, File) getUriForFile()}. Notice that this
* method does <b>not</b> throw an {@link IOException}; you must check its return value.
*
* @param uri A content URI for a file, as returned by
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
* @param selection Ignored. Set to {@code null}.
* @param selectionArgs Ignored. Set to {@code null}.
* @return 1 if the delete succeeds; otherwise, 0.
*/
@Override
public int delete(@NonNull Uri uri, @Nullable String selection,
@Nullable String[] selectionArgs) {
// ContentProvider has already checked granted permissions
// final File file = mStrategy.getFileForUri(uri);
// return file.delete() ? 1 : 0;
throw new UnsupportedOperationException("No external delete");
}
/**
* By default, FileProvider automatically returns the
* {@link ParcelFileDescriptor} for a file associated with a <code>content://</code>
* {@link Uri}. To get the {@link ParcelFileDescriptor}, call
* {@link ContentResolver#openFileDescriptor(Uri, String)
* ContentResolver.openFileDescriptor}.
*
* To override this method, you must provide your own subclass of FileProvider.
*
* @param uri A content URI associated with a file, as returned by
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
* @param mode Access mode for the file. May be "r" for read-only access, "rw" for read and
* write access, or "rwt" for read and write access that truncates any existing file.
* @return A new {@link ParcelFileDescriptor} with which you can access the file.
*/
@SuppressLint("UnknownNullness") // b/171012356
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
throws FileNotFoundException {
// ContentProvider has already checked granted permissions
final File file = mStrategy.getFileForUri(uri);
final int fileMode = modeToMode(mode);
return ParcelFileDescriptor.open(file, fileMode);
}
/**
* Return {@link PathStrategy} for given authority, either by parsing or
* returning from cache.
*/
private static PathStrategy getPathStrategy(Context context, String authority, int resourceId) {
PathStrategy strat;
synchronized (sCache) {
strat = sCache.get(authority);
if (strat == null) {
try {
strat = parsePathStrategy(context, authority, resourceId);
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
} catch (XmlPullParserException e) {
throw new IllegalArgumentException(
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
}
sCache.put(authority, strat);
}
}
return strat;
}
@VisibleForTesting
static XmlResourceParser getFileProviderPathsMetaData(Context context, String authority,
@Nullable ProviderInfo info,
int resourceId) {
if (info == null) {
throw new IllegalArgumentException(
"Couldn't find meta-data for provider with authority " + authority);
}
if (info.metaData == null && resourceId != ResourcesCompat.ID_NULL) {
info.metaData = new Bundle(1);
info.metaData.putInt(META_DATA_FILE_PROVIDER_PATHS, resourceId);
}
final XmlResourceParser in = info.loadXmlMetaData(
context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
if (in == null) {
throw new IllegalArgumentException(
"Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data");
}
return in;
}
/**
* Parse and return {@link PathStrategy} for given authority as defined in
* {@link #META_DATA_FILE_PROVIDER_PATHS} {@code <meta-data>}.
*
* @see #getPathStrategy(Context, String, int)
*/
private static PathStrategy parsePathStrategy(Context context, String authority, int resourceId)
throws IOException, XmlPullParserException {
final SimplePathStrategy strat = new SimplePathStrategy(authority);
final ProviderInfo info = context.getPackageManager()
.resolveContentProvider(authority, PackageManager.GET_META_DATA);
final XmlResourceParser in = getFileProviderPathsMetaData(context, authority, info,
resourceId);
int type;
while ((type = in.next()) != END_DOCUMENT) {
if (type == START_TAG) {
final String tag = in.getName();
final String name = in.getAttributeValue(null, ATTR_NAME);
String path = in.getAttributeValue(null, ATTR_PATH);
File target = null;
if (TAG_ROOT_PATH.equals(tag)) {
target = DEVICE_ROOT;
} else if (TAG_FILES_PATH.equals(tag)) {
target = context.getFilesDir();
} else if (TAG_CACHE_PATH.equals(tag)) {
target = context.getCacheDir();
} else if (TAG_EXTERNAL.equals(tag)) {
target = Environment.getExternalStorageDirectory();
} else if (TAG_EXTERNAL_FILES.equals(tag)) {
File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null);
if (externalFilesDirs.length > 0) {
target = externalFilesDirs[0];
}
} else if (TAG_EXTERNAL_CACHE.equals(tag)) {
File[] externalCacheDirs = ContextCompat.getExternalCacheDirs(context);
if (externalCacheDirs.length > 0) {
target = externalCacheDirs[0];
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& TAG_EXTERNAL_MEDIA.equals(tag)) {
File[] externalMediaDirs = Api21Impl.getExternalMediaDirs(context);
if (externalMediaDirs.length > 0) {
target = externalMediaDirs[0];
}
}
if (target != null) {
strat.addRoot(name, buildPath(target, path));
}
}
}
return strat;
}
/**
* Strategy for mapping between {@link File} and {@link Uri}.
* <p>
* Strategies must be symmetric so that mapping a {@link File} to a
* {@link Uri} and then back to a {@link File} points at the original
* target.
* <p>
* Strategies must remain consistent across app launches, and not rely on
* dynamic state. This ensures that any generated {@link Uri} can still be
* resolved if your process is killed and later restarted.
*
* @see SimplePathStrategy
*/
interface PathStrategy {
/**
* Return a {@link Uri} that represents the given {@link File}.
*/
Uri getUriForFile(File file);
/**
* Return a {@link File} that represents the given {@link Uri}.
*/
File getFileForUri(Uri uri);
}
/**
* Strategy that provides access to files living under a narrow allowed list
* of filesystem roots. It will throw {@link SecurityException} if callers try
* accessing files outside the configured roots.
* <p>
* For example, if configured with
* {@code addRoot("myfiles", context.getFilesDir())}, then
* {@code context.getFileStreamPath("foo.txt")} would map to
* {@code content://myauthority/myfiles/foo.txt}.
*/
static class SimplePathStrategy implements PathStrategy {
private final String mAuthority;
private final HashMap<String, File> mRoots = new HashMap<>();
SimplePathStrategy(String authority) {
mAuthority = authority;
}
/**
* Add a mapping from a name to a filesystem root. The provider only offers
* access to files that live under configured roots.
*/
void addRoot(String name, File root) {
if (TextUtils.isEmpty(name)) {
throw new IllegalArgumentException("Name must not be empty");
}
try {
// Resolve to canonical path to keep path checking fast
root = root.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to resolve canonical path for " + root, e);
}
mRoots.put(name, root);
}
@Override
public Uri getUriForFile(File file) {
String path;
try {
path = file.getCanonicalPath();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
}
// Find the most-specific root path
Map.Entry<String, File> mostSpecific = null;
for (Map.Entry<String, File> root : mRoots.entrySet()) {
final String rootPath = root.getValue().getPath();
if (path.startsWith(rootPath) && (mostSpecific == null
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
mostSpecific = root;
}
}
if (mostSpecific == null) {
throw new IllegalArgumentException(
"Failed to find configured root that contains " + path);
}
// Start at first char of path under root
final String rootPath = mostSpecific.getValue().getPath();
if (rootPath.endsWith("/")) {
path = path.substring(rootPath.length());
} else {
path = path.substring(rootPath.length() + 1);
}
// Encode the tag and path separately
path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
return new Uri.Builder().scheme("content")
.authority(mAuthority).encodedPath(path).build();
}
@Override
public File getFileForUri(Uri uri) {
String path = uri.getEncodedPath();
final int splitIndex = path.indexOf('/', 1);
final String tag = Uri.decode(path.substring(1, splitIndex));
path = Uri.decode(path.substring(splitIndex + 1));
final File root = mRoots.get(tag);
if (root == null) {
throw new IllegalArgumentException("Unable to find configured root for " + uri);
}
File file = new File(root, path);
try {
file = file.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
}
if (!file.getPath().startsWith(root.getPath())) {
throw new SecurityException("Resolved path jumped beyond configured root");
}
return file;
}
}
/**
* Copied from ContentResolver.java
*/
private static int modeToMode(String mode) {
int modeBits;
if ("r".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_READ_ONLY;
// } else if ("w".equals(mode) || "wt".equals(mode)) {
// modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
// | ParcelFileDescriptor.MODE_CREATE
// | ParcelFileDescriptor.MODE_TRUNCATE;
// } else if ("wa".equals(mode)) {
// modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
// | ParcelFileDescriptor.MODE_CREATE
// | ParcelFileDescriptor.MODE_APPEND;
// } else if ("rw".equals(mode)) {
// modeBits = ParcelFileDescriptor.MODE_READ_WRITE
// | ParcelFileDescriptor.MODE_CREATE;
// } else if ("rwt".equals(mode)) {
// modeBits = ParcelFileDescriptor.MODE_READ_WRITE
// | ParcelFileDescriptor.MODE_CREATE
// | ParcelFileDescriptor.MODE_TRUNCATE;
} else {
throw new IllegalArgumentException("Invalid mode: " + mode);
}
return modeBits;
}
private static File buildPath(File base, String... segments) {
File cur = base;
for (String segment : segments) {
if (segment != null) {
cur = new File(cur, segment);
}
}
return cur;
}
private static String[] copyOf(String[] original, int newLength) {
final String[] result = new String[newLength];
System.arraycopy(original, 0, result, 0, newLength);
return result;
}
private static Object[] copyOf(Object[] original, int newLength) {
final Object[] result = new Object[newLength];
System.arraycopy(original, 0, result, 0, newLength);
return result;
}
@RequiresApi(21)
static class Api21Impl {
private Api21Impl() {
// This class is not instantiable.
}
static File[] getExternalMediaDirs(Context context) {
// Deprecated, otherwise this would belong on ContextCompat as a public method.
return context.getExternalMediaDirs();
}
}
}
Then the AndroidManifest:
<provider
android:name=".fileprovider.ExportedFileProvider"
android:authorities="${applicationId}.fileprovider.exported"
android:exported="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/exported_filepaths" />
</provider>
The exported_filepaths.xml:
You should choose which path to expose based on your needs, but always remember to include subdirectories. Otherwise, all directories can be accessed by any 3rd-party apps.
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="internal_cache" path="exported" />
</paths>
Now you can use ExportedFileProvider to generate the Uri you need. Simply ensure to place the files in the correct exposed directory.
There is further discussion of this issue in my blog. You can also refer to it, but this answer should already sufficiently address your question: https://medium.com/@wanxiao1994/how-to-implement-a-carousel-style-notification-b95fb817d2ba
Upvotes: 1
Reputation: 111
As always, the answer is simple, but the lack of knowledge makes it that much harder. The "Image Uri" that "RemoteViews.setImageViewUri()" is expecting is the non-FileUriExposedException
version of Uri
. Now I can tell the story how I find out what is the "non-FileUriExposedException
" version of Uri
.
Initially, I tried to pass just an absolute path as a Uri
(I knew that I was using abs. path, but then suddenly forgot this):
val image_uri = Uri.parse(return_absolute_file_path_to_image())
But this didn't work. Then I prepended "content://"
string to the return value — still no good. In the process I was able to have a small experience using Glide
and Picasso
libraries. I found a solution like this:
internal fun update(
context: Context,
app_widget_manager: AppWidgetManager,
app_widget_id: Int,
) {
val image_uri = Uri.parse("file://" + return_absolute_file_path_to_image())
// val image_uri = File(return_absolute_file_path_to_image()).toUri()
views.setImageViewUri(R.id.imageView, image_uri)
Picasso.get()
.load(image_uri)
.into(views, R.id.imageView, intArrayOf(app_widget_id))
app_widget_manager.updateAppWidget(app_widget_id, views)
}
The difference is that "content://"
has changed to "file://"
or we can use File()
constructor instead. We can also overwrite what goes into .into()
method (heh) in such a way that we can overwrite methods that are responsible for error handling etc. (this is just Picasso things). This does work, but probably a bad way to go about the issue (because file's Uri
is still kind of exposed, but I'm not sure).
And it was a pretty clean solution, but I had to add +1 implementation dependency into build.gradle
(:app).
Finally, I saw Uri similar to this one: content://$provider/files/#
. And I also saw that I can actually override openFile(uri: Uri, mode: String): ParcelFileDescriptor?
method in ContentProvider
's child class of mine (apparently ContentProvider
does not require an overridden implementation of this method, but I can still do that). Basically, this is the method that should be called when "home screen" activity tries to get the image Uri
. This method takes in previously mentioned Uri (content://$provider/files/#
), but of course the part after content provider can be anything you like, even the absolute path of the image. And this method does not expose file's real URI (file:///path/to/image.png
), that is why everything started to work as it should be. Now the function looks like this:
internal fun update(
context: Context,
app_widget_manager: AppWidgetManager,
app_widget_id: Int,
) {
val views = RemoteViews(context.packageName, R.layout.gallery_slideshow)
var image_uri = get_next_image_uri(context)
views.setImageViewUri(R.id.imageView, image_uri)
app_widget_manager.updateAppWidget(app_widget_id, views)
}
and get_next_image_uri(Context)
:
internal fun get_next_image_uri(context: Context): Uri? {
val authority = "${context.packageName}.provider"
val uri_prefix = "content://$authority"
val content_provider_uri = Uri.parse(uri_prefix)
val cursor = context.contentResolver.query(
content_provider_uri, null, null, null, null
)
cursor!!.moveToFirst()
val gallery_image_path = cursor.getString(0)
cursor.close()
return Uri.parse("$uri_prefix/files/$gallery_image_path")
}
and the update()
is called from onUpdate()
:
class MyWidgetClass : AppWidgetProvider() {
override fun onUpdate(
context: Context,
app_widget_manager: AppWidgetManager,
app_widget_id_array: IntArray,
) {
for (app_widget_id in app_widget_id_array) {
update(context, app_widget_manager, app_widget_id)
}
}
...
}
So, there are 2 times, when something has to be fetched by using a Uri
:
context.contentResolver.query(content_provider_uri, null, null, null, null)
;views.setImageViewUri(R.id.imageView, image_uri)
Both Uri
s are handled by the same class GalleryImagePathProvider : ContentProvider() {}
. In the first case it is handled through the override fun query()
method, and in the second case — through the override fun openFile()
method. Now, in order for everything to work properly, we need to add some "magic" to the AndroidManifest.xml
:
<manifest ... >
...
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
...
<application ... >
...
<provider
android:name=".GalleryImagePathProvider"
android:authorities="${applicationId}.provider"
android:exported="true"
tools:ignore="ExportedContentProvider" />
</application>
</manifest ... >
The key point here is android:exported="true"
, without it "home screen" activity can't display images (at least in my case). (Permission to read external storage is used when accessing system gallery's images.)
That's all, I think I went over all the important things and problems that I had to solve to achieve the goal.
P.S. I don't want to make a whole promotion, but (at this point I kinda have to) when I posted this question, I got the
Calling all who code. Take the 2023 Developer Survey.
notification and when I got to the questions about AI tools/language models, I found quite a big list of bots or search engines. I then laid my eyes on You.com
text. And almost instantly (free and \wo any limits), I finally got to know how it feels when you have a "companion" that tries hard to solve all your coding questions and problems. And thanks to YouBot, I was able to crunch down a whole a lot of time of debugging by simply listening to the advices and suggestions from it. So, as a fellow coder, I can recommend it. (But sometime prompt input lag becomes really big, and after some time in standby mode you have to refresh the tab for it to be able to work properly again. Everything else is super nice and cool.)
Upvotes: 0