Merge "Revert "Cache binder LocalCallingIdentity across requests"" into rvc-dev
diff --git a/apex/framework/Android.bp b/apex/framework/Android.bp
index 6f0aeba..62104a2 100644
--- a/apex/framework/Android.bp
+++ b/apex/framework/Android.bp
@@ -55,7 +55,6 @@
name: "framework-mediaprovider-stubs-srcs-defaults",
srcs: [
":framework-mediaprovider-sources",
- ":framework-mediaprovider-annotation-sources",
],
sdk_version: "system_current",
}
diff --git a/apex/framework/api/current.txt b/apex/framework/api/current.txt
new file mode 100644
index 0000000..52439fb
--- /dev/null
+++ b/apex/framework/api/current.txt
@@ -0,0 +1,404 @@
+// Signature format: 2.0
+package android.provider {
+
+ public final class MediaStore {
+ ctor public MediaStore();
+ method @NonNull public static android.app.PendingIntent createDeleteRequest(@NonNull android.content.ContentResolver, @NonNull java.util.Collection<android.net.Uri>);
+ method @NonNull public static android.app.PendingIntent createFavoriteRequest(@NonNull android.content.ContentResolver, @NonNull java.util.Collection<android.net.Uri>, boolean);
+ method @NonNull public static android.app.PendingIntent createTrashRequest(@NonNull android.content.ContentResolver, @NonNull java.util.Collection<android.net.Uri>, boolean);
+ method @NonNull public static android.app.PendingIntent createWriteRequest(@NonNull android.content.ContentResolver, @NonNull java.util.Collection<android.net.Uri>);
+ method @Nullable public static android.net.Uri getDocumentUri(@NonNull android.content.Context, @NonNull android.net.Uri);
+ method @NonNull public static java.util.Set<java.lang.String> getExternalVolumeNames(@NonNull android.content.Context);
+ method public static long getGeneration(@NonNull android.content.Context, @NonNull String);
+ method public static android.net.Uri getMediaScannerUri();
+ method @Nullable public static android.net.Uri getMediaUri(@NonNull android.content.Context, @NonNull android.net.Uri);
+ method @NonNull public static java.util.Set<java.lang.String> getRecentExternalVolumeNames(@NonNull android.content.Context);
+ method public static boolean getRequireOriginal(@NonNull android.net.Uri);
+ method @NonNull public static String getVersion(@NonNull android.content.Context);
+ method @NonNull public static String getVersion(@NonNull android.content.Context, @NonNull String);
+ method @NonNull public static String getVolumeName(@NonNull android.net.Uri);
+ method @Deprecated @NonNull public static android.net.Uri setIncludePending(@NonNull android.net.Uri);
+ method @NonNull public static android.net.Uri setRequireOriginal(@NonNull android.net.Uri);
+ field public static final String ACTION_IMAGE_CAPTURE = "android.media.action.IMAGE_CAPTURE";
+ field public static final String ACTION_IMAGE_CAPTURE_SECURE = "android.media.action.IMAGE_CAPTURE_SECURE";
+ field public static final String ACTION_REVIEW = "android.provider.action.REVIEW";
+ field public static final String ACTION_REVIEW_SECURE = "android.provider.action.REVIEW_SECURE";
+ field public static final String ACTION_VIDEO_CAPTURE = "android.media.action.VIDEO_CAPTURE";
+ field public static final String AUTHORITY = "media";
+ field @NonNull public static final android.net.Uri AUTHORITY_URI;
+ field public static final String EXTRA_BRIGHTNESS = "android.provider.extra.BRIGHTNESS";
+ field public static final String EXTRA_DURATION_LIMIT = "android.intent.extra.durationLimit";
+ field public static final String EXTRA_FINISH_ON_COMPLETION = "android.intent.extra.finishOnCompletion";
+ field public static final String EXTRA_FULL_SCREEN = "android.intent.extra.fullScreen";
+ field public static final String EXTRA_MEDIA_ALBUM = "android.intent.extra.album";
+ field public static final String EXTRA_MEDIA_ARTIST = "android.intent.extra.artist";
+ field public static final String EXTRA_MEDIA_FOCUS = "android.intent.extra.focus";
+ field public static final String EXTRA_MEDIA_GENRE = "android.intent.extra.genre";
+ field public static final String EXTRA_MEDIA_PLAYLIST = "android.intent.extra.playlist";
+ field public static final String EXTRA_MEDIA_RADIO_CHANNEL = "android.intent.extra.radio_channel";
+ field public static final String EXTRA_MEDIA_TITLE = "android.intent.extra.title";
+ field public static final String EXTRA_OUTPUT = "output";
+ field public static final String EXTRA_SCREEN_ORIENTATION = "android.intent.extra.screenOrientation";
+ field public static final String EXTRA_SHOW_ACTION_ICONS = "android.intent.extra.showActionIcons";
+ field public static final String EXTRA_SIZE_LIMIT = "android.intent.extra.sizeLimit";
+ field public static final String EXTRA_VIDEO_QUALITY = "android.intent.extra.videoQuality";
+ field public static final String INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH = "android.media.action.MEDIA_PLAY_FROM_SEARCH";
+ field public static final String INTENT_ACTION_MEDIA_SEARCH = "android.intent.action.MEDIA_SEARCH";
+ field @Deprecated public static final String INTENT_ACTION_MUSIC_PLAYER = "android.intent.action.MUSIC_PLAYER";
+ field public static final String INTENT_ACTION_STILL_IMAGE_CAMERA = "android.media.action.STILL_IMAGE_CAMERA";
+ field public static final String INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE = "android.media.action.STILL_IMAGE_CAMERA_SECURE";
+ field public static final String INTENT_ACTION_TEXT_OPEN_FROM_SEARCH = "android.media.action.TEXT_OPEN_FROM_SEARCH";
+ field public static final String INTENT_ACTION_VIDEO_CAMERA = "android.media.action.VIDEO_CAMERA";
+ field public static final String INTENT_ACTION_VIDEO_PLAY_FROM_SEARCH = "android.media.action.VIDEO_PLAY_FROM_SEARCH";
+ field public static final int MATCH_DEFAULT = 0; // 0x0
+ field public static final int MATCH_EXCLUDE = 2; // 0x2
+ field public static final int MATCH_INCLUDE = 1; // 0x1
+ field public static final int MATCH_ONLY = 3; // 0x3
+ field public static final String MEDIA_IGNORE_FILENAME = ".nomedia";
+ field public static final String MEDIA_SCANNER_VOLUME = "volume";
+ field public static final String META_DATA_REVIEW_GALLERY_PREWARM_SERVICE = "android.media.review_gallery_prewarm_service";
+ field public static final String META_DATA_STILL_IMAGE_CAMERA_PREWARM_SERVICE = "android.media.still_image_camera_preview_service";
+ field public static final String QUERY_ARG_MATCH_FAVORITE = "android:query-arg-match-favorite";
+ field public static final String QUERY_ARG_MATCH_PENDING = "android:query-arg-match-pending";
+ field public static final String QUERY_ARG_MATCH_TRASHED = "android:query-arg-match-trashed";
+ field public static final String QUERY_ARG_RELATED_URI = "android:query-arg-related-uri";
+ field public static final String UNKNOWN_STRING = "<unknown>";
+ field public static final String VOLUME_EXTERNAL = "external";
+ field public static final String VOLUME_EXTERNAL_PRIMARY = "external_primary";
+ field public static final String VOLUME_INTERNAL = "internal";
+ }
+
+ public static final class MediaStore.Audio {
+ ctor public MediaStore.Audio();
+ method @Deprecated @Nullable public static String keyFor(@Nullable String);
+ }
+
+ public static interface MediaStore.Audio.AlbumColumns {
+ field public static final String ALBUM = "album";
+ field @Deprecated public static final String ALBUM_ART = "album_art";
+ field public static final String ALBUM_ID = "album_id";
+ field @Deprecated public static final String ALBUM_KEY = "album_key";
+ field public static final String ARTIST = "artist";
+ field public static final String ARTIST_ID = "artist_id";
+ field @Deprecated public static final String ARTIST_KEY = "artist_key";
+ field public static final String FIRST_YEAR = "minyear";
+ field public static final String LAST_YEAR = "maxyear";
+ field public static final String NUMBER_OF_SONGS = "numsongs";
+ field public static final String NUMBER_OF_SONGS_FOR_ARTIST = "numsongs_by_artist";
+ }
+
+ public static final class MediaStore.Audio.Albums implements android.provider.BaseColumns android.provider.MediaStore.Audio.AlbumColumns {
+ ctor public MediaStore.Audio.Albums();
+ method public static android.net.Uri getContentUri(String);
+ field public static final String CONTENT_TYPE = "vnd.android.cursor.dir/albums";
+ field public static final String DEFAULT_SORT_ORDER = "album_key";
+ field public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/album";
+ field public static final android.net.Uri EXTERNAL_CONTENT_URI;
+ field public static final android.net.Uri INTERNAL_CONTENT_URI;
+ }
+
+ public static interface MediaStore.Audio.ArtistColumns {
+ field public static final String ARTIST = "artist";
+ field @Deprecated public static final String ARTIST_KEY = "artist_key";
+ field public static final String NUMBER_OF_ALBUMS = "number_of_albums";
+ field public static final String NUMBER_OF_TRACKS = "number_of_tracks";
+ }
+
+ public static final class MediaStore.Audio.Artists implements android.provider.BaseColumns android.provider.MediaStore.Audio.ArtistColumns {
+ ctor public MediaStore.Audio.Artists();
+ method public static android.net.Uri getContentUri(String);
+ field public static final String CONTENT_TYPE = "vnd.android.cursor.dir/artists";
+ field public static final String DEFAULT_SORT_ORDER = "artist_key";
+ field public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/artist";
+ field public static final android.net.Uri EXTERNAL_CONTENT_URI;
+ field public static final android.net.Uri INTERNAL_CONTENT_URI;
+ }
+
+ public static final class MediaStore.Audio.Artists.Albums implements android.provider.MediaStore.Audio.AlbumColumns {
+ ctor public MediaStore.Audio.Artists.Albums();
+ method public static android.net.Uri getContentUri(String, long);
+ }
+
+ public static interface MediaStore.Audio.AudioColumns extends android.provider.MediaStore.MediaColumns {
+ field public static final String ALBUM_ID = "album_id";
+ field @Deprecated public static final String ALBUM_KEY = "album_key";
+ field public static final String ARTIST_ID = "artist_id";
+ field @Deprecated public static final String ARTIST_KEY = "artist_key";
+ field public static final String BOOKMARK = "bookmark";
+ field public static final String GENRE = "genre";
+ field public static final String GENRE_ID = "genre_id";
+ field @Deprecated public static final String GENRE_KEY = "genre_key";
+ field public static final String IS_ALARM = "is_alarm";
+ field public static final String IS_AUDIOBOOK = "is_audiobook";
+ field public static final String IS_MUSIC = "is_music";
+ field public static final String IS_NOTIFICATION = "is_notification";
+ field public static final String IS_PODCAST = "is_podcast";
+ field public static final String IS_RINGTONE = "is_ringtone";
+ field @Deprecated public static final String TITLE_KEY = "title_key";
+ field public static final String TITLE_RESOURCE_URI = "title_resource_uri";
+ field public static final String TRACK = "track";
+ field public static final String YEAR = "year";
+ }
+
+ public static final class MediaStore.Audio.Genres implements android.provider.BaseColumns android.provider.MediaStore.Audio.GenresColumns {
+ ctor public MediaStore.Audio.Genres();
+ method public static android.net.Uri getContentUri(String);
+ method public static android.net.Uri getContentUriForAudioId(String, int);
+ field public static final String CONTENT_TYPE = "vnd.android.cursor.dir/genre";
+ field public static final String DEFAULT_SORT_ORDER = "name";
+ field public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/genre";
+ field public static final android.net.Uri EXTERNAL_CONTENT_URI;
+ field public static final android.net.Uri INTERNAL_CONTENT_URI;
+ }
+
+ public static final class MediaStore.Audio.Genres.Members implements android.provider.MediaStore.Audio.AudioColumns {
+ ctor public MediaStore.Audio.Genres.Members();
+ method public static android.net.Uri getContentUri(String, long);
+ field public static final String AUDIO_ID = "audio_id";
+ field public static final String CONTENT_DIRECTORY = "members";
+ field public static final String DEFAULT_SORT_ORDER = "title_key";
+ field public static final String GENRE_ID = "genre_id";
+ }
+
+ public static interface MediaStore.Audio.GenresColumns {
+ field public static final String NAME = "name";
+ }
+
+ public static final class MediaStore.Audio.Media implements android.provider.MediaStore.Audio.AudioColumns {
+ ctor public MediaStore.Audio.Media();
+ method public static android.net.Uri getContentUri(String);
+ method @NonNull public static android.net.Uri getContentUri(@NonNull String, long);
+ method @Deprecated @Nullable public static android.net.Uri getContentUriForPath(@NonNull String);
+ field public static final String CONTENT_TYPE = "vnd.android.cursor.dir/audio";
+ field public static final String DEFAULT_SORT_ORDER = "title_key";
+ field public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/audio";
+ field public static final android.net.Uri EXTERNAL_CONTENT_URI;
+ field public static final String EXTRA_MAX_BYTES = "android.provider.MediaStore.extra.MAX_BYTES";
+ field public static final android.net.Uri INTERNAL_CONTENT_URI;
+ field public static final String RECORD_SOUND_ACTION = "android.provider.MediaStore.RECORD_SOUND";
+ }
+
+ public static final class MediaStore.Audio.Playlists implements android.provider.BaseColumns android.provider.MediaStore.Audio.PlaylistsColumns {
+ ctor public MediaStore.Audio.Playlists();
+ method public static android.net.Uri getContentUri(String);
+ field public static final String CONTENT_TYPE = "vnd.android.cursor.dir/playlist";
+ field public static final String DEFAULT_SORT_ORDER = "name";
+ field public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/playlist";
+ field public static final android.net.Uri EXTERNAL_CONTENT_URI;
+ field public static final android.net.Uri INTERNAL_CONTENT_URI;
+ }
+
+ public static final class MediaStore.Audio.Playlists.Members implements android.provider.MediaStore.Audio.AudioColumns {
+ ctor public MediaStore.Audio.Playlists.Members();
+ method public static android.net.Uri getContentUri(String, long);
+ method public static boolean moveItem(android.content.ContentResolver, long, int, int);
+ field public static final String AUDIO_ID = "audio_id";
+ field public static final String CONTENT_DIRECTORY = "members";
+ field public static final String DEFAULT_SORT_ORDER = "play_order";
+ field public static final String PLAYLIST_ID = "playlist_id";
+ field public static final String PLAY_ORDER = "play_order";
+ field public static final String _ID = "_id";
+ }
+
+ public static interface MediaStore.Audio.PlaylistsColumns extends android.provider.MediaStore.MediaColumns {
+ field @Deprecated public static final String DATA = "_data";
+ field public static final String DATE_ADDED = "date_added";
+ field public static final String DATE_MODIFIED = "date_modified";
+ field public static final String NAME = "name";
+ }
+
+ public static final class MediaStore.Audio.Radio {
+ field public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/radio";
+ }
+
+ public static interface MediaStore.DownloadColumns extends android.provider.MediaStore.MediaColumns {
+ field public static final String DOWNLOAD_URI = "download_uri";
+ field public static final String REFERER_URI = "referer_uri";
+ }
+
+ public static final class MediaStore.Downloads implements android.provider.MediaStore.DownloadColumns {
+ method @NonNull public static android.net.Uri getContentUri(@NonNull String);
+ method @NonNull public static android.net.Uri getContentUri(@NonNull String, long);
+ field public static final String CONTENT_TYPE = "vnd.android.cursor.dir/download";
+ field @NonNull public static final android.net.Uri EXTERNAL_CONTENT_URI;
+ field @NonNull public static final android.net.Uri INTERNAL_CONTENT_URI;
+ }
+
+ public static final class MediaStore.Files {
+ ctor public MediaStore.Files();
+ method public static android.net.Uri getContentUri(String);
+ method public static android.net.Uri getContentUri(String, long);
+ }
+
+ public static interface MediaStore.Files.FileColumns extends android.provider.MediaStore.MediaColumns {
+ field public static final String MEDIA_TYPE = "media_type";
+ field public static final int MEDIA_TYPE_AUDIO = 2; // 0x2
+ field public static final int MEDIA_TYPE_DOCUMENT = 6; // 0x6
+ field public static final int MEDIA_TYPE_IMAGE = 1; // 0x1
+ field public static final int MEDIA_TYPE_NONE = 0; // 0x0
+ field public static final int MEDIA_TYPE_PLAYLIST = 4; // 0x4
+ field public static final int MEDIA_TYPE_SUBTITLE = 5; // 0x5
+ field public static final int MEDIA_TYPE_VIDEO = 3; // 0x3
+ field public static final String MIME_TYPE = "mime_type";
+ field public static final String PARENT = "parent";
+ }
+
+ public static final class MediaStore.Images {
+ ctor public MediaStore.Images();
+ }
+
+ public static interface MediaStore.Images.ImageColumns extends android.provider.MediaStore.MediaColumns {
+ field public static final String DESCRIPTION = "description";
+ field public static final String EXPOSURE_TIME = "exposure_time";
+ field public static final String F_NUMBER = "f_number";
+ field public static final String ISO = "iso";
+ field public static final String IS_PRIVATE = "isprivate";
+ field @Deprecated public static final String LATITUDE = "latitude";
+ field @Deprecated public static final String LONGITUDE = "longitude";
+ field @Deprecated public static final String MINI_THUMB_MAGIC = "mini_thumb_magic";
+ field @Deprecated public static final String PICASA_ID = "picasa_id";
+ field public static final String SCENE_CAPTURE_TYPE = "scene_capture_type";
+ }
+
+ public static final class MediaStore.Images.Media implements android.provider.MediaStore.Images.ImageColumns {
+ ctor public MediaStore.Images.Media();
+ method @Deprecated public static android.graphics.Bitmap getBitmap(android.content.ContentResolver, android.net.Uri) throws java.io.FileNotFoundException, java.io.IOException;
+ method public static android.net.Uri getContentUri(String);
+ method @NonNull public static android.net.Uri getContentUri(@NonNull String, long);
+ method @Deprecated public static String insertImage(android.content.ContentResolver, String, String, String) throws java.io.FileNotFoundException;
+ method @Deprecated public static String insertImage(android.content.ContentResolver, android.graphics.Bitmap, String, String);
+ method @Deprecated public static android.database.Cursor query(android.content.ContentResolver, android.net.Uri, String[]);
+ method @Deprecated public static android.database.Cursor query(android.content.ContentResolver, android.net.Uri, String[], String, String);
+ method @Deprecated public static android.database.Cursor query(android.content.ContentResolver, android.net.Uri, String[], String, String[], String);
+ field public static final String CONTENT_TYPE = "vnd.android.cursor.dir/image";
+ field public static final String DEFAULT_SORT_ORDER = "bucket_display_name";
+ field public static final android.net.Uri EXTERNAL_CONTENT_URI;
+ field public static final android.net.Uri INTERNAL_CONTENT_URI;
+ }
+
+ @Deprecated public static class MediaStore.Images.Thumbnails implements android.provider.BaseColumns {
+ ctor @Deprecated public MediaStore.Images.Thumbnails();
+ method @Deprecated public static void cancelThumbnailRequest(android.content.ContentResolver, long);
+ method @Deprecated public static void cancelThumbnailRequest(android.content.ContentResolver, long, long);
+ method @Deprecated public static android.net.Uri getContentUri(String);
+ method @Deprecated @NonNull public static android.util.Size getKindSize(int);
+ method @Deprecated public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, int, android.graphics.BitmapFactory.Options);
+ method @Deprecated public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, long, int, android.graphics.BitmapFactory.Options);
+ method @Deprecated public static final android.database.Cursor query(android.content.ContentResolver, android.net.Uri, String[]);
+ method @Deprecated public static final android.database.Cursor queryMiniThumbnail(android.content.ContentResolver, long, int, String[]);
+ method @Deprecated public static final android.database.Cursor queryMiniThumbnails(android.content.ContentResolver, android.net.Uri, int, String[]);
+ field @Deprecated public static final String DATA = "_data";
+ field @Deprecated public static final String DEFAULT_SORT_ORDER = "image_id ASC";
+ field @Deprecated public static final android.net.Uri EXTERNAL_CONTENT_URI;
+ field @Deprecated public static final int FULL_SCREEN_KIND = 2; // 0x2
+ field @Deprecated public static final String HEIGHT = "height";
+ field @Deprecated public static final String IMAGE_ID = "image_id";
+ field @Deprecated public static final android.net.Uri INTERNAL_CONTENT_URI;
+ field @Deprecated public static final String KIND = "kind";
+ field @Deprecated public static final int MICRO_KIND = 3; // 0x3
+ field @Deprecated public static final int MINI_KIND = 1; // 0x1
+ field @Deprecated public static final String THUMB_DATA = "thumb_data";
+ field @Deprecated public static final String WIDTH = "width";
+ }
+
+ public static interface MediaStore.MediaColumns extends android.provider.BaseColumns {
+ field public static final String ALBUM = "album";
+ field public static final String ALBUM_ARTIST = "album_artist";
+ field public static final String ARTIST = "artist";
+ field public static final String AUTHOR = "author";
+ field public static final String BITRATE = "bitrate";
+ field public static final String BUCKET_DISPLAY_NAME = "bucket_display_name";
+ field public static final String BUCKET_ID = "bucket_id";
+ field public static final String CAPTURE_FRAMERATE = "capture_framerate";
+ field public static final String CD_TRACK_NUMBER = "cd_track_number";
+ field public static final String COMPILATION = "compilation";
+ field public static final String COMPOSER = "composer";
+ field @Deprecated public static final String DATA = "_data";
+ field public static final String DATE_ADDED = "date_added";
+ field public static final String DATE_EXPIRES = "date_expires";
+ field public static final String DATE_MODIFIED = "date_modified";
+ field public static final String DATE_TAKEN = "datetaken";
+ field public static final String DISC_NUMBER = "disc_number";
+ field public static final String DISPLAY_NAME = "_display_name";
+ field public static final String DOCUMENT_ID = "document_id";
+ field public static final String DURATION = "duration";
+ field public static final String GENERATION_ADDED = "generation_added";
+ field public static final String GENERATION_MODIFIED = "generation_modified";
+ field public static final String GENRE = "genre";
+ field public static final String HEIGHT = "height";
+ field public static final String INSTANCE_ID = "instance_id";
+ field public static final String IS_DOWNLOAD = "is_download";
+ field public static final String IS_DRM = "is_drm";
+ field public static final String IS_FAVORITE = "is_favorite";
+ field public static final String IS_PENDING = "is_pending";
+ field public static final String IS_TRASHED = "is_trashed";
+ field public static final String MIME_TYPE = "mime_type";
+ field public static final String NUM_TRACKS = "num_tracks";
+ field public static final String ORIENTATION = "orientation";
+ field public static final String ORIGINAL_DOCUMENT_ID = "original_document_id";
+ field public static final String OWNER_PACKAGE_NAME = "owner_package_name";
+ field public static final String RELATIVE_PATH = "relative_path";
+ field public static final String RESOLUTION = "resolution";
+ field public static final String SIZE = "_size";
+ field public static final String TITLE = "title";
+ field public static final String VOLUME_NAME = "volume_name";
+ field public static final String WIDTH = "width";
+ field public static final String WRITER = "writer";
+ field public static final String XMP = "xmp";
+ field public static final String YEAR = "year";
+ }
+
+ public static final class MediaStore.Video {
+ ctor public MediaStore.Video();
+ method @Deprecated public static android.database.Cursor query(android.content.ContentResolver, android.net.Uri, String[]);
+ field public static final String DEFAULT_SORT_ORDER = "_display_name";
+ }
+
+ public static final class MediaStore.Video.Media implements android.provider.MediaStore.Video.VideoColumns {
+ ctor public MediaStore.Video.Media();
+ method public static android.net.Uri getContentUri(String);
+ method @NonNull public static android.net.Uri getContentUri(@NonNull String, long);
+ field public static final String CONTENT_TYPE = "vnd.android.cursor.dir/video";
+ field public static final String DEFAULT_SORT_ORDER = "title";
+ field public static final android.net.Uri EXTERNAL_CONTENT_URI;
+ field public static final android.net.Uri INTERNAL_CONTENT_URI;
+ }
+
+ @Deprecated public static class MediaStore.Video.Thumbnails implements android.provider.BaseColumns {
+ ctor @Deprecated public MediaStore.Video.Thumbnails();
+ method @Deprecated public static void cancelThumbnailRequest(android.content.ContentResolver, long);
+ method @Deprecated public static void cancelThumbnailRequest(android.content.ContentResolver, long, long);
+ method @Deprecated public static android.net.Uri getContentUri(String);
+ method @Deprecated @NonNull public static android.util.Size getKindSize(int);
+ method @Deprecated public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, int, android.graphics.BitmapFactory.Options);
+ method @Deprecated public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, long, int, android.graphics.BitmapFactory.Options);
+ field @Deprecated public static final String DATA = "_data";
+ field @Deprecated public static final String DEFAULT_SORT_ORDER = "video_id ASC";
+ field @Deprecated public static final android.net.Uri EXTERNAL_CONTENT_URI;
+ field @Deprecated public static final int FULL_SCREEN_KIND = 2; // 0x2
+ field @Deprecated public static final String HEIGHT = "height";
+ field @Deprecated public static final android.net.Uri INTERNAL_CONTENT_URI;
+ field @Deprecated public static final String KIND = "kind";
+ field @Deprecated public static final int MICRO_KIND = 3; // 0x3
+ field @Deprecated public static final int MINI_KIND = 1; // 0x1
+ field @Deprecated public static final String VIDEO_ID = "video_id";
+ field @Deprecated public static final String WIDTH = "width";
+ }
+
+ public static interface MediaStore.Video.VideoColumns extends android.provider.MediaStore.MediaColumns {
+ field public static final String BOOKMARK = "bookmark";
+ field public static final String CATEGORY = "category";
+ field public static final String COLOR_RANGE = "color_range";
+ field public static final String COLOR_STANDARD = "color_standard";
+ field public static final String COLOR_TRANSFER = "color_transfer";
+ field public static final String DESCRIPTION = "description";
+ field public static final String IS_PRIVATE = "isprivate";
+ field public static final String LANGUAGE = "language";
+ field @Deprecated public static final String LATITUDE = "latitude";
+ field @Deprecated public static final String LONGITUDE = "longitude";
+ field @Deprecated public static final String MINI_THUMB_MAGIC = "mini_thumb_magic";
+ field public static final String TAGS = "tags";
+ }
+
+}
+
diff --git a/apex/framework/api/module-lib-current.txt b/apex/framework/api/module-lib-current.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/apex/framework/api/module-lib-current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/framework/api/module-lib-removed.txt b/apex/framework/api/module-lib-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/apex/framework/api/module-lib-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/framework/api/removed.txt b/apex/framework/api/removed.txt
new file mode 100644
index 0000000..183a7c9
--- /dev/null
+++ b/apex/framework/api/removed.txt
@@ -0,0 +1,43 @@
+// Signature format: 2.0
+package android.provider {
+
+ public static interface MediaStore.Audio.AudioColumns extends android.provider.MediaStore.MediaColumns {
+ field public static final String ALBUM = "album";
+ field public static final String ARTIST = "artist";
+ field public static final String COMPOSER = "composer";
+ field public static final String DURATION = "duration";
+ }
+
+ public static interface MediaStore.DownloadColumns extends android.provider.MediaStore.MediaColumns {
+ field @Deprecated public static final String DESCRIPTION = "description";
+ }
+
+ public static interface MediaStore.Files.FileColumns extends android.provider.MediaStore.MediaColumns {
+ field public static final String TITLE = "title";
+ }
+
+ public static interface MediaStore.Images.ImageColumns extends android.provider.MediaStore.MediaColumns {
+ field public static final String BUCKET_DISPLAY_NAME = "bucket_display_name";
+ field public static final String BUCKET_ID = "bucket_id";
+ field public static final String DATE_TAKEN = "datetaken";
+ field public static final String GROUP_ID = "group_id";
+ field public static final String ORIENTATION = "orientation";
+ }
+
+ public static interface MediaStore.MediaColumns extends android.provider.BaseColumns {
+ field @Deprecated public static final String GROUP_ID = "group_id";
+ }
+
+ public static interface MediaStore.Video.VideoColumns extends android.provider.MediaStore.MediaColumns {
+ field public static final String ALBUM = "album";
+ field public static final String ARTIST = "artist";
+ field public static final String BUCKET_DISPLAY_NAME = "bucket_display_name";
+ field public static final String BUCKET_ID = "bucket_id";
+ field public static final String DATE_TAKEN = "datetaken";
+ field public static final String DURATION = "duration";
+ field public static final String GROUP_ID = "group_id";
+ field public static final String RESOLUTION = "resolution";
+ }
+
+}
+
diff --git a/apex/framework/api/system-current.txt b/apex/framework/api/system-current.txt
new file mode 100644
index 0000000..5ce4218
--- /dev/null
+++ b/apex/framework/api/system-current.txt
@@ -0,0 +1,14 @@
+// Signature format: 2.0
+package android.provider {
+
+ public final class MediaStore {
+ method @NonNull public static android.net.Uri rewriteToLegacy(@NonNull android.net.Uri);
+ method @NonNull @WorkerThread public static android.net.Uri scanFile(@NonNull android.content.ContentResolver, @NonNull java.io.File);
+ method @WorkerThread public static void scanVolume(@NonNull android.content.ContentResolver, @NonNull String);
+ method @WorkerThread public static void waitForIdle(@NonNull android.content.ContentResolver);
+ field public static final String AUTHORITY_LEGACY = "media_legacy";
+ field @NonNull public static final android.net.Uri AUTHORITY_LEGACY_URI;
+ }
+
+}
+
diff --git a/apex/framework/api/system-removed.txt b/apex/framework/api/system-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/apex/framework/api/system-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/gen_strings.py b/gen_strings.py
old mode 100644
new mode 100755
index ee06841..99bba8a
--- a/gen_strings.py
+++ b/gen_strings.py
@@ -29,7 +29,7 @@
for verb in verbs:
verblabel = verb
if verb == "write":
- verblabel = "change"
+ verblabel = "modify"
verblabelcaps = verblabel[0].upper() + verblabel[1:]
if verb == "trash":
@@ -45,15 +45,8 @@
print Template('''
<!-- Dialog title asking if user will allow $verb permission to the $data item displayed below this string. [CHAR LIMIT=128] -->
<plurals name="permission_${verb}_${data}">
- <item quantity="one">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> move this $datalabel to trash?</item>
- <item quantity="other">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> move <xliff:g id="count" example="42">^2</xliff:g> ${datalabel}s to trash?</item>
-</plurals>
-''').substitute(vars()).strip("\n")
- print Template('''
-<!-- Dialog body text explaining that this $data item will be permanently deleted after the shown duration. [CHAR LIMIT=128] -->
-<plurals name="permission_${verb}_${data}_info">
- <item quantity="one">This $datalabel will be permanently deleted after <xliff:g id="duration" example="42">^3</xliff:g> days</item>
- <item quantity="other">These ${datalabel}s will be permanently deleted after <xliff:g id="duration" example="42">^3</xliff:g> days</item>
+ <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move this $datalabel to trash?</item>
+ <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move <xliff:g id="count" example="42">^2</xliff:g> ${datalabel}s to trash?</item>
</plurals>
''').substitute(vars()).strip("\n")
@@ -61,8 +54,8 @@
print Template('''
<!-- Dialog title asking if user will allow $verb permission to the $data item displayed below this string. [CHAR LIMIT=128] -->
<plurals name="permission_${verb}_${data}">
- <item quantity="one">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> move this $datalabel out of trash?</item>
- <item quantity="other">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> move <xliff:g id="count" example="42">^2</xliff:g> ${datalabel}s out of trash?</item>
+ <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move this $datalabel out of trash?</item>
+ <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move <xliff:g id="count" example="42">^2</xliff:g> ${datalabel}s out of trash?</item>
</plurals>
''').substitute(vars()).strip("\n")
@@ -70,19 +63,11 @@
print Template('''
<!-- Dialog title asking if user will allow $verb permission to the $data item displayed below this string. [CHAR LIMIT=128] -->
<plurals name="permission_${verb}_${data}">
- <item quantity="one">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> $verblabel this $datalabel?</item>
- <item quantity="other">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> $verblabel <xliff:g id="count" example="42">^2</xliff:g> ${datalabel}s?</item>
+ <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to $verblabel this $datalabel?</item>
+ <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to $verblabel <xliff:g id="count" example="42">^2</xliff:g> ${datalabel}s?</item>
</plurals>
''').substitute(vars()).strip("\n")
-
- print Template('''
-<!-- Positive dialog button confirming that $verb permission should be granted. [CHAR LIMIT=32] -->
-<string name="permission_${verb}_grant">${verblabelcaps}</string>
-<!-- Negative dialog button confirming that $verb permission should not be granted. [CHAR LIMIT=32] -->
-<string name="permission_${verb}_deny">Cancel</string>
-''').substitute(vars()).strip("\n")
-
print '''
<!-- ========================= END AUTO-GENERATED BY gen_strings.py ========================= -->
'''
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index a014489..d014a8e 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -816,7 +816,7 @@
// FUSE after that write may be served from cache
bool direct_io = ri->isRedactionNeeded() || is_file_locked(fd, path);
- handle* h = new handle(path, fd, ri, /*owner_uid*/ -1, !direct_io);
+ handle* h = new handle(path, fd, ri, !direct_io);
node->AddHandle(h);
return h;
}
@@ -1080,11 +1080,6 @@
fuse->fadviser.Close(h->fd);
if (node) {
- // TODO(b/145737191) Legacy apps don't expect FuseDaemon to update database.
- // Inserting/deleting the database entry might break app's functionality.
- // if (h->owner_uid != -1) {
- // fuse->mp->ScanFile(h->path, h->owner_uid);
- // }
node->DestroyHandle(h);
}
diff --git a/jni/MediaProviderWrapper.cpp b/jni/MediaProviderWrapper.cpp
index 0cdfd87..77e1470 100644
--- a/jni/MediaProviderWrapper.cpp
+++ b/jni/MediaProviderWrapper.cpp
@@ -116,9 +116,9 @@
}
void scanFileInternal(JNIEnv* env, jobject media_provider_object, jmethodID mid_scan_file,
- const string& path, uid_t uid) {
+ const string& path) {
ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
- env->CallVoidMethod(media_provider_object, mid_scan_file, j_path.get(), uid);
+ env->CallVoidMethod(media_provider_object, mid_scan_file, j_path.get());
CheckForJniException(env);
}
@@ -241,7 +241,7 @@
mid_delete_file_ = CacheMethod(env, "deleteFile", "(Ljava/lang/String;I)I", /*is_static*/ false);
mid_is_open_allowed_ = CacheMethod(env, "isOpenAllowed", "(Ljava/lang/String;IZ)I",
/*is_static*/ false);
- mid_scan_file_ = CacheMethod(env, "scanFile", "(Ljava/lang/String;I)V",
+ mid_scan_file_ = CacheMethod(env, "scanFile", "(Ljava/lang/String;)V",
/*is_static*/ false);
mid_is_mkdir_or_rmdir_allowed_ = CacheMethod(env, "isDirectoryCreationOrDeletionAllowed",
"(Ljava/lang/String;IZ)I", /*is_static*/ false);
@@ -290,7 +290,7 @@
int MediaProviderWrapper::DeleteFile(const string& path, uid_t uid) {
if (shouldBypassMediaProvider(uid)) {
int res = unlink(path.c_str());
- ScanFile(path, uid);
+ ScanFile(path);
return res;
}
@@ -308,15 +308,9 @@
for_write);
}
-void MediaProviderWrapper::ScanFile(const string& path, uid_t uid) {
- if (uid == 0) {
- // uid = 0 is kernel or adb shell running as root and doesn't have a package associated with
- // it. This uid should get global access hence use MediaProvider's uid.
- uid = getuid();
- }
-
+void MediaProviderWrapper::ScanFile(const string& path) {
JNIEnv* env = MaybeAttachCurrentThread();
- scanFileInternal(env, media_provider_object_, mid_scan_file_, path, uid);
+ scanFileInternal(env, media_provider_object_, mid_scan_file_, path);
}
int MediaProviderWrapper::IsCreatingDirAllowed(const string& path, uid_t uid) {
diff --git a/jni/MediaProviderWrapper.h b/jni/MediaProviderWrapper.h
index 22c5b3e..016696f 100644
--- a/jni/MediaProviderWrapper.h
+++ b/jni/MediaProviderWrapper.h
@@ -105,9 +105,8 @@
* MediaProvider database.
*
* @param path the path of the file to be scanned
- * @param uid UID of the app that owns the file on the given path
*/
- void ScanFile(const std::string& path, uid_t uid);
+ void ScanFile(const std::string& path);
/**
* Determines if the given UID is allowed to create a directory with the given path.
diff --git a/jni/node-inl.h b/jni/node-inl.h
index 5098316..a146d20 100644
--- a/jni/node-inl.h
+++ b/jni/node-inl.h
@@ -36,16 +36,14 @@
namespace fuse {
struct handle {
- explicit handle(const std::string& path, int fd, const RedactionInfo* ri, uid_t owner_uid,
- bool cached)
- : path(path), fd(fd), ri(ri), owner_uid(owner_uid), cached(cached) {
+ explicit handle(const std::string& path, int fd, const RedactionInfo* ri, bool cached)
+ : path(path), fd(fd), ri(ri), cached(cached) {
CHECK(ri != nullptr);
}
const std::string path;
const int fd;
const std::unique_ptr<const RedactionInfo> ri;
- const uid_t owner_uid;
const bool cached;
~handle() { close(fd); }
diff --git a/jni/node_test.cpp b/jni/node_test.cpp
index 423af3d..fd56f49 100644
--- a/jni/node_test.cpp
+++ b/jni/node_test.cpp
@@ -213,8 +213,7 @@
TEST_F(NodeTest, AddDestroyHandle) {
unique_node_ptr node = CreateNode(nullptr, "/path");
- handle* h = new handle("/path", -1, new mediaprovider::fuse::RedactionInfo, getuid(),
- true /* cached */);
+ handle* h = new handle("/path", -1, new mediaprovider::fuse::RedactionInfo, true /* cached */);
node->AddHandle(h);
ASSERT_TRUE(node->HasCachedHandle());
@@ -225,7 +224,7 @@
// the node in question.
EXPECT_DEATH(node->DestroyHandle(h), "");
EXPECT_DEATH(node->DestroyHandle(nullptr), "");
- std::unique_ptr<handle> h2(new handle("/path2", -1, new mediaprovider::fuse::RedactionInfo,
- getuid(), true /* cached */));
+ std::unique_ptr<handle> h2(
+ new handle("/path2", -1, new mediaprovider::fuse::RedactionInfo, true /* cached */));
EXPECT_DEATH(node->DestroyHandle(h2.get()), "");
}
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 04cab66..ef51112 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -51,6 +51,7 @@
<!-- Text placed over a visual thumbnail indicating that there are more items beyond the number currently displayed on the screen. [CHAR LIMIT=6] -->
<plurals name="permission_more_thumb">
+ <item quantity="one">+<xliff:g id="count" example="1">^1</xliff:g></item>
<item quantity="other">+<xliff:g id="count" example="42">^1</xliff:g></item>
</plurals>
@@ -61,10 +62,10 @@
</plurals>
<!-- Cache clearing permission dialog warning title. [CHAR LIMIT=NONE] -->
- <string name="cache_clearing_dialog_title">Clear app cache with <xliff:g id="app_seeking_permission" example="File manager">%s</xliff:g>?</string>
+ <string name="cache_clearing_dialog_title">Clear temporary app files?</string>
<!-- Cache clearing permission dialog warning text. [CHAR LIMIT=NONE] -->
- <string name="cache_clearing_dialog_text"><xliff:g id="app_seeking_permission" example="File manager">%s</xliff:g> would like to clear some temporary files. Accepting may increase battery or data use.</string>
+ <string name="cache_clearing_dialog_text"><xliff:g id="app_seeking_permission" example="File manager">%s</xliff:g> would like to clear some temporary files. This may result in an increased usage of battery or cellular data.</string>
<!-- Allow dialog action text. [CHAR LIMIT=30] -->
<string name="allow">Allow</string>
@@ -78,129 +79,93 @@
<!-- Dialog title asking if user will allow write permission to the audio item displayed below this string. [CHAR LIMIT=128] -->
<plurals name="permission_write_audio">
- <item quantity="one">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> change this audio file?</item>
- <item quantity="other">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> change <xliff:g id="count" example="42">^2</xliff:g> audio files?</item>
+ <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to modify this audio file?</item>
+ <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to modify <xliff:g id="count" example="42">^2</xliff:g> audio files?</item>
</plurals>
<!-- Dialog title asking if user will allow write permission to the video item displayed below this string. [CHAR LIMIT=128] -->
<plurals name="permission_write_video">
- <item quantity="one">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> change this video?</item>
- <item quantity="other">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> change <xliff:g id="count" example="42">^2</xliff:g> videos?</item>
+ <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to modify this video?</item>
+ <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to modify <xliff:g id="count" example="42">^2</xliff:g> videos?</item>
</plurals>
<!-- Dialog title asking if user will allow write permission to the image item displayed below this string. [CHAR LIMIT=128] -->
<plurals name="permission_write_image">
- <item quantity="one">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> change this photo?</item>
- <item quantity="other">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> change <xliff:g id="count" example="42">^2</xliff:g> photos?</item>
+ <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to modify this photo?</item>
+ <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to modify <xliff:g id="count" example="42">^2</xliff:g> photos?</item>
</plurals>
<!-- Dialog title asking if user will allow write permission to the generic item displayed below this string. [CHAR LIMIT=128] -->
<plurals name="permission_write_generic">
- <item quantity="one">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> change this item?</item>
- <item quantity="other">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> change <xliff:g id="count" example="42">^2</xliff:g> items?</item>
+ <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to modify this item?</item>
+ <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to modify <xliff:g id="count" example="42">^2</xliff:g> items?</item>
</plurals>
- <!-- Positive dialog button confirming that write permission should be granted. [CHAR LIMIT=32] -->
- <string name="permission_write_grant">Change</string>
- <!-- Negative dialog button confirming that write permission should not be granted. [CHAR LIMIT=32] -->
- <string name="permission_write_deny">Cancel</string>
<!-- ========================= TRASH STRINGS ========================= -->
<!-- Dialog title asking if user will allow trash permission to the audio item displayed below this string. [CHAR LIMIT=128] -->
<plurals name="permission_trash_audio">
- <item quantity="one">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> move this audio file to trash?</item>
- <item quantity="other">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> move <xliff:g id="count" example="42">^2</xliff:g> audio files to trash?</item>
- </plurals>
- <!-- Dialog body text explaining that this audio item will be permanently deleted after the shown duration. [CHAR LIMIT=128] -->
- <plurals name="permission_trash_audio_info">
- <item quantity="one">This audio file will be permanently deleted after <xliff:g id="duration" example="42">^3</xliff:g> days</item>
- <item quantity="other">These audio files will be permanently deleted after <xliff:g id="duration" example="42">^3</xliff:g> days</item>
+ <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move this audio file to trash?</item>
+ <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move <xliff:g id="count" example="42">^2</xliff:g> audio files to trash?</item>
</plurals>
<!-- Dialog title asking if user will allow trash permission to the video item displayed below this string. [CHAR LIMIT=128] -->
<plurals name="permission_trash_video">
- <item quantity="one">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> move this video to trash?</item>
- <item quantity="other">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> move <xliff:g id="count" example="42">^2</xliff:g> videos to trash?</item>
- </plurals>
- <!-- Dialog body text explaining that this video item will be permanently deleted after the shown duration. [CHAR LIMIT=128] -->
- <plurals name="permission_trash_video_info">
- <item quantity="one">This video will be permanently deleted after <xliff:g id="duration" example="42">^3</xliff:g> days</item>
- <item quantity="other">These videos will be permanently deleted after <xliff:g id="duration" example="42">^3</xliff:g> days</item>
+ <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move this video to trash?</item>
+ <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move <xliff:g id="count" example="42">^2</xliff:g> videos to trash?</item>
</plurals>
<!-- Dialog title asking if user will allow trash permission to the image item displayed below this string. [CHAR LIMIT=128] -->
<plurals name="permission_trash_image">
- <item quantity="one">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> move this photo to trash?</item>
- <item quantity="other">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> move <xliff:g id="count" example="42">^2</xliff:g> photos to trash?</item>
- </plurals>
- <!-- Dialog body text explaining that this image item will be permanently deleted after the shown duration. [CHAR LIMIT=128] -->
- <plurals name="permission_trash_image_info">
- <item quantity="one">This photo will be permanently deleted after <xliff:g id="duration" example="42">^3</xliff:g> days</item>
- <item quantity="other">These photos will be permanently deleted after <xliff:g id="duration" example="42">^3</xliff:g> days</item>
+ <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move this photo to trash?</item>
+ <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move <xliff:g id="count" example="42">^2</xliff:g> photos to trash?</item>
</plurals>
<!-- Dialog title asking if user will allow trash permission to the generic item displayed below this string. [CHAR LIMIT=128] -->
<plurals name="permission_trash_generic">
- <item quantity="one">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> move this item to trash?</item>
- <item quantity="other">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> move <xliff:g id="count" example="42">^2</xliff:g> items to trash?</item>
+ <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move this item to trash?</item>
+ <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move <xliff:g id="count" example="42">^2</xliff:g> items to trash?</item>
</plurals>
- <!-- Dialog body text explaining that this generic item will be permanently deleted after the shown duration. [CHAR LIMIT=128] -->
- <plurals name="permission_trash_generic_info">
- <item quantity="one">This item will be permanently deleted after <xliff:g id="duration" example="42">^3</xliff:g> days</item>
- <item quantity="other">These items will be permanently deleted after <xliff:g id="duration" example="42">^3</xliff:g> days</item>
- </plurals>
- <!-- Positive dialog button confirming that trash permission should be granted. [CHAR LIMIT=32] -->
- <string name="permission_trash_grant">Move to trash</string>
- <!-- Negative dialog button confirming that trash permission should not be granted. [CHAR LIMIT=32] -->
- <string name="permission_trash_deny">Cancel</string>
<!-- ========================= UNTRASH STRINGS ========================= -->
<!-- Dialog title asking if user will allow untrash permission to the audio item displayed below this string. [CHAR LIMIT=128] -->
<plurals name="permission_untrash_audio">
- <item quantity="one">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> move this audio file out of trash?</item>
- <item quantity="other">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> move <xliff:g id="count" example="42">^2</xliff:g> audio files out of trash?</item>
+ <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move this audio file out of trash?</item>
+ <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move <xliff:g id="count" example="42">^2</xliff:g> audio files out of trash?</item>
</plurals>
<!-- Dialog title asking if user will allow untrash permission to the video item displayed below this string. [CHAR LIMIT=128] -->
<plurals name="permission_untrash_video">
- <item quantity="one">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> move this video out of trash?</item>
- <item quantity="other">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> move <xliff:g id="count" example="42">^2</xliff:g> videos out of trash?</item>
+ <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move this video out of trash?</item>
+ <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move <xliff:g id="count" example="42">^2</xliff:g> videos out of trash?</item>
</plurals>
<!-- Dialog title asking if user will allow untrash permission to the image item displayed below this string. [CHAR LIMIT=128] -->
<plurals name="permission_untrash_image">
- <item quantity="one">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> move this photo out of trash?</item>
- <item quantity="other">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> move <xliff:g id="count" example="42">^2</xliff:g> photos out of trash?</item>
+ <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move this photo out of trash?</item>
+ <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move <xliff:g id="count" example="42">^2</xliff:g> photos out of trash?</item>
</plurals>
<!-- Dialog title asking if user will allow untrash permission to the generic item displayed below this string. [CHAR LIMIT=128] -->
<plurals name="permission_untrash_generic">
- <item quantity="one">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> move this item out of trash?</item>
- <item quantity="other">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> move <xliff:g id="count" example="42">^2</xliff:g> items out of trash?</item>
+ <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move this item out of trash?</item>
+ <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move <xliff:g id="count" example="42">^2</xliff:g> items out of trash?</item>
</plurals>
- <!-- Positive dialog button confirming that untrash permission should be granted. [CHAR LIMIT=32] -->
- <string name="permission_untrash_grant">Move out of trash</string>
- <!-- Negative dialog button confirming that untrash permission should not be granted. [CHAR LIMIT=32] -->
- <string name="permission_untrash_deny">Cancel</string>
<!-- ========================= DELETE STRINGS ========================= -->
<!-- Dialog title asking if user will allow delete permission to the audio item displayed below this string. [CHAR LIMIT=128] -->
<plurals name="permission_delete_audio">
- <item quantity="one">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> delete this audio file?</item>
- <item quantity="other">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> delete <xliff:g id="count" example="42">^2</xliff:g> audio files?</item>
+ <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to delete this audio file?</item>
+ <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to delete <xliff:g id="count" example="42">^2</xliff:g> audio files?</item>
</plurals>
<!-- Dialog title asking if user will allow delete permission to the video item displayed below this string. [CHAR LIMIT=128] -->
<plurals name="permission_delete_video">
- <item quantity="one">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> delete this video?</item>
- <item quantity="other">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> delete <xliff:g id="count" example="42">^2</xliff:g> videos?</item>
+ <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to delete this video?</item>
+ <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to delete <xliff:g id="count" example="42">^2</xliff:g> videos?</item>
</plurals>
<!-- Dialog title asking if user will allow delete permission to the image item displayed below this string. [CHAR LIMIT=128] -->
<plurals name="permission_delete_image">
- <item quantity="one">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> delete this photo?</item>
- <item quantity="other">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> delete <xliff:g id="count" example="42">^2</xliff:g> photos?</item>
+ <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to delete this photo?</item>
+ <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to delete <xliff:g id="count" example="42">^2</xliff:g> photos?</item>
</plurals>
<!-- Dialog title asking if user will allow delete permission to the generic item displayed below this string. [CHAR LIMIT=128] -->
<plurals name="permission_delete_generic">
- <item quantity="one">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> delete this item?</item>
- <item quantity="other">Let <xliff:g id="app_name" example="Gmail">^1</xliff:g> delete <xliff:g id="count" example="42">^2</xliff:g> items?</item>
+ <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to delete this item?</item>
+ <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to delete <xliff:g id="count" example="42">^2</xliff:g> items?</item>
</plurals>
- <!-- Positive dialog button confirming that delete permission should be granted. [CHAR LIMIT=32] -->
- <string name="permission_delete_grant">Delete</string>
- <!-- Negative dialog button confirming that delete permission should not be granted. [CHAR LIMIT=32] -->
- <string name="permission_delete_deny">Cancel</string>
<!-- ========================= END AUTO-GENERATED BY gen_strings.py ========================= -->
diff --git a/src/com/android/providers/media/LocalCallingIdentity.java b/src/com/android/providers/media/LocalCallingIdentity.java
index ef6c27f..f767dcd 100644
--- a/src/com/android/providers/media/LocalCallingIdentity.java
+++ b/src/com/android/providers/media/LocalCallingIdentity.java
@@ -37,8 +37,6 @@
import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.app.compat.CompatChanges;
-import android.compat.annotation.ChangeId;
-import android.compat.annotation.Disabled;
import android.content.ContentProvider;
import android.content.Context;
import android.content.pm.ApplicationInfo;
@@ -69,39 +67,13 @@
}
/**
- * Scoped Storage is on by default. However, it is not strictly enforced and there are multiple
- * ways to opt out of scoped storage:
- * <ul>
- * <li>Target Sdk < Q</li>
- * <li>Target Sdk = Q and has `requestLegacyExternalStorage` set in AndroidManifest.xml</li>
- * <li>Target Sdk > Q: Upgrading from an app that was opted out of scoped storage and has
- * `preserveLegacyExternalStorage` set in AndroidManifest.xml</li>
- * </ul>
- * This flag is enabled for all apps by default as Scoped Storage is enabled by default.
- * Developers can disable this flag to opt out of Scoped Storage and have legacy storage
- * workflow.
- *
- * Note: {@code FORCE_ENABLE_SCOPED_STORAGE} should also be disabled for apps to opt out of
- * scoped storage.
- * See https://developer.android.com/training/data-storage#scoped-storage for more information.
+ * See definition in {@link android.os.Environment}
*/
- @ChangeId
private static final long DEFAULT_SCOPED_STORAGE = 149924527L;
/**
- * Setting this flag strictly enforces Scoped Storage regardless of:
- * <ul>
- * <li>The value of Target Sdk</li>
- * <li>The value of `requestLegacyExternalStorage` in AndroidManifest.xml</li>
- * <li>The value of `preserveLegacyExternalStorage` in AndroidManifest.xml</li>
- * </ul>
- *
- * Note: {@code DEFAULT_SCOPED_STORAGE} should also be enabled for apps to be enforced into
- * scoped storage.
- * See https://developer.android.com/training/data-storage#scoped-storage for more information.
+ * See definition in {@link android.os.Environment}
*/
- @ChangeId
- @Disabled
private static final long FORCE_ENABLE_SCOPED_STORAGE = 132649864L;
public static LocalCallingIdentity fromBinder(Context context, ContentProvider provider) {
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 67b7d86..79357c7 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -64,7 +64,10 @@
import static com.android.providers.media.util.FileUtils.extractRelativePathForDirectory;
import static com.android.providers.media.util.FileUtils.extractTopLevelDir;
import static com.android.providers.media.util.FileUtils.extractVolumeName;
+import static com.android.providers.media.util.FileUtils.getAbsoluteSanitizedPath;
import static com.android.providers.media.util.FileUtils.isDownload;
+import static com.android.providers.media.util.FileUtils.sanitizeDisplayName;
+import static com.android.providers.media.util.FileUtils.sanitizePath;
import static com.android.providers.media.util.Logging.LOGV;
import static com.android.providers.media.util.Logging.TAG;
import static com.android.providers.media.util.PermissionUtils.checkPermissionManageExternalStorage;
@@ -997,15 +1000,12 @@
/**
* Makes MediaScanner scan the given file.
* @param file path of the file to be scanned
- * @param uid UID of the app that owns the file on the given path. If the file is scanned
- * on create, this UID will be used for updating owner package.
*
* Called from JNI in jni/MediaProviderWrapper.cpp
*/
@Keep
- public void scanFileForFuse(String file, int uid) {
- final String callingPackage = getCachedCallingIdentityForFuse(uid).getPackageName();
- scanFile(new File(file), REASON_DEMAND, callingPackage);
+ public void scanFileForFuse(String file) {
+ scanFile(new File(file), REASON_DEMAND);
}
/**
@@ -2301,34 +2301,6 @@
Trace.endSection();
}
- private static @NonNull String[] sanitizePath(@Nullable String path) {
- if (path == null) {
- return new String[0];
- } else {
- final String[] segments = path.split("/");
- // If the path corresponds to the top level directory, then we return an empty path
- // which denotes the top level directory
- if (segments.length == 0) {
- return new String[] { "" };
- }
- for (int i = 0; i < segments.length; i++) {
- segments[i] = sanitizeDisplayName(segments[i]);
- }
- return segments;
- }
- }
-
- private static @Nullable String sanitizeDisplayName(@Nullable String name) {
- if (name == null) {
- return null;
- } else if (name.startsWith(".")) {
- // The resulting file must not be hidden.
- return FileUtils.buildValidFatFilename("_" + name);
- } else {
- return FileUtils.buildValidFatFilename(name);
- }
- }
-
/**
* Sanity check that any requested {@link MediaColumns#DATA} paths actually
* live on the storage volume being targeted.
@@ -5426,16 +5398,6 @@
}
}
- @Nullable
- private String getAbsoluteSanitizedPath(String path) {
- final String[] pathSegments = sanitizePath(path);
- if (pathSegments.length == 0) {
- return null;
- }
- return path = "/" + String.join("/",
- Arrays.copyOfRange(pathSegments, 1, pathSegments.length));
- }
-
/**
* Calculates the ranges that need to be redacted for the given file and user that wants to
* access the file.
diff --git a/src/com/android/providers/media/PermissionActivity.java b/src/com/android/providers/media/PermissionActivity.java
index 49a0062..371522a 100644
--- a/src/com/android/providers/media/PermissionActivity.java
+++ b/src/com/android/providers/media/PermissionActivity.java
@@ -147,8 +147,8 @@
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(resolveTitleText());
- builder.setPositiveButton(resolvePositiveText(), this::onPositiveAction);
- builder.setNegativeButton(resolveNegativeText(), this::onNegativeAction);
+ builder.setPositiveButton(R.string.allow, this::onPositiveAction);
+ builder.setNegativeButton(R.string.deny, this::onNegativeAction);
builder.setCancelable(false);
builder.setView(bodyView);
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index 81a6a34..955c027 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -384,7 +384,8 @@
final Bundle queryArgs = new Bundle();
queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
formatClause + " AND " + dataClause + " AND " + generationClause);
- final String pathEscapedForLike = DatabaseUtils.escapeForLike(mRoot.getAbsolutePath());
+ final String pathEscapedForLike = DatabaseUtils.escapeForLike(
+ FileUtils.getAbsoluteSanitizedPath(mRoot.getAbsolutePath()));
queryArgs.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
new String[] {pathEscapedForLike + "/%", pathEscapedForLike});
queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER,
diff --git a/src/com/android/providers/media/util/ExifUtils.java b/src/com/android/providers/media/util/ExifUtils.java
index 09674a8..7593ec8 100644
--- a/src/com/android/providers/media/util/ExifUtils.java
+++ b/src/com/android/providers/media/util/ExifUtils.java
@@ -32,6 +32,7 @@
import android.annotation.Nullable;
import android.media.ExifInterface;
+import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import java.text.ParsePosition;
@@ -49,7 +50,9 @@
// Pattern to check non zero timestamp
private static final Pattern sNonZeroTimePattern = Pattern.compile(".*[1-9].*");
+ @GuardedBy("sFormatter")
private static final SimpleDateFormat sFormatter;
+ @GuardedBy("sFormatterTz")
private static final SimpleDateFormat sFormatterTz;
static {
@@ -105,7 +108,10 @@
ParsePosition pos = new ParsePosition(0);
try {
- Date datetime = sFormatter.parse(dateTimeString, pos);
+ final Date datetime;
+ synchronized (sFormatter) {
+ datetime = sFormatter.parse(dateTimeString, pos);
+ }
if (datetime == null) return -1;
return datetime.getTime();
} catch (IllegalArgumentException e) {
@@ -122,12 +128,17 @@
try {
// The exif field is in local time. Parsing it as if it is UTC will yield time
// since 1/1/1970 local time
- Date datetime = sFormatter.parse(dateTimeString, pos);
+ Date datetime;
+ synchronized (sFormatter) {
+ datetime = sFormatter.parse(dateTimeString, pos);
+ }
if (offsetString != null) {
dateTimeString = dateTimeString + " " + offsetString;
ParsePosition position = new ParsePosition(0);
- datetime = sFormatterTz.parse(dateTimeString, position);
+ synchronized (sFormatterTz) {
+ datetime = sFormatterTz.parse(dateTimeString, position);
+ }
}
if (datetime == null) return -1;
diff --git a/src/com/android/providers/media/util/FileUtils.java b/src/com/android/providers/media/util/FileUtils.java
index b247f2e..c9cf1e2 100644
--- a/src/com/android/providers/media/util/FileUtils.java
+++ b/src/com/android/providers/media/util/FileUtils.java
@@ -701,4 +701,49 @@
}
}
}
+
+ /** {@hide} **/
+ @Nullable
+ public static String getAbsoluteSanitizedPath(String path) {
+ final String[] pathSegments = sanitizePath(path);
+ if (pathSegments.length == 0) {
+ return null;
+ }
+ return path = "/" + String.join("/",
+ Arrays.copyOfRange(pathSegments, 1, pathSegments.length));
+ }
+
+ /** {@hide} */
+ public static @NonNull String[] sanitizePath(@Nullable String path) {
+ if (path == null) {
+ return new String[0];
+ } else {
+ final String[] segments = path.split("/");
+ // If the path corresponds to the top level directory, then we return an empty path
+ // which denotes the top level directory
+ if (segments.length == 0) {
+ return new String[] { "" };
+ }
+ for (int i = 0; i < segments.length; i++) {
+ segments[i] = sanitizeDisplayName(segments[i]);
+ }
+ return segments;
+ }
+ }
+
+ /**
+ * Sanitizes given name by appending '_' to make it non-hidden and mutating the file
+ * name to make it valid for a FAT filesystem.
+ * @hide
+ */
+ public static @Nullable String sanitizeDisplayName(@Nullable String name) {
+ if (name == null) {
+ return null;
+ } else if (name.startsWith(".")) {
+ // The resulting file must not be hidden.
+ return buildValidFatFilename("_" + name);
+ } else {
+ return buildValidFatFilename(name);
+ }
+ }
}
diff --git a/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java b/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
index 94c2a74..886acd8 100644
--- a/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
+++ b/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
@@ -20,6 +20,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import android.app.UiAutomation;
import android.content.ContentProviderClient;
@@ -32,6 +33,7 @@
import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
import android.os.storage.StorageManager;
import android.provider.BaseColumns;
import android.provider.MediaStore;
@@ -59,6 +61,7 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
/**
* Verify that we preserve information from the old "legacy" provider from
@@ -74,6 +77,9 @@
// TODO: expand test to cover secondary storage devices
private String mVolumeName = MediaStore.VOLUME_EXTERNAL_PRIMARY;
+ private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
+ private static final long POLLING_SLEEP_MILLIS = 100;
+
private Uri mExternalAudio;
private Uri mExternalVideo;
private Uri mExternalImages;
@@ -211,9 +217,7 @@
// Clear data on the modern provider so that the initial scan recovers
// metadata from the legacy provider
executeShellCommand("pm clear " + modernProvider.applicationInfo.packageName, ui);
-
- // Sleep to give the MediaProvider time to recover from being killed after clearing data
- Thread.sleep(5000);
+ pollForExternalStorageState();
// And force a scan to confirm upgraded data survives
MediaStore.waitForIdle(context.getContentResolver());
@@ -243,6 +247,17 @@
}
}
+ private static void pollForExternalStorageState() throws Exception {
+ for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) {
+ if(Environment.getExternalStorageState(Environment.getExternalStorageDirectory())
+ .equals(Environment.MEDIA_MOUNTED)) {
+ return;
+ }
+ SystemClock.sleep(POLLING_SLEEP_MILLIS);
+ }
+ fail("Timed out while waiting for ExternalStorageState to be MEDIA_MOUNTED");
+ }
+
public static String executeShellCommand(String command, UiAutomation uiAutomation)
throws IOException {
Log.v(TAG, "$ " + command);
diff --git a/tests/src/com/android/providers/media/util/ExifUtilsTest.java b/tests/src/com/android/providers/media/util/ExifUtilsTest.java
new file mode 100644
index 0000000..2b0724c
--- /dev/null
+++ b/tests/src/com/android/providers/media/util/ExifUtilsTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.util;
+
+import android.media.ExifInterface;
+
+import org.junit.Test;
+
+import java.io.File;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.ToLongFunction;
+
+import static org.junit.Assert.assertEquals;
+
+public class ExifUtilsTest {
+ @Test
+ public void testParseDateTime() throws Exception {
+ final ExifInterface exif = createTestExif();
+ assertParseDateTime(exif, ExifUtils::getDateTimeOriginal);
+ }
+
+ @Test
+ public void testParseDateTimeTz() throws Exception {
+ final ExifInterface exif = createTestExif();
+ assertParseDateTime(exif, ExifUtils::getDateTimeDigitized);
+ }
+
+ @Test
+ public void testParseGpsDateTime() throws Exception {
+ final ExifInterface exif = createTestExif();
+ assertParseDateTime(exif, ExifUtils::getGpsDateTime);
+ }
+
+ private ExifInterface createTestExif() throws Exception {
+ final File file = File.createTempFile("test", ".jpg");
+ final ExifInterface exif = new ExifInterface(file);
+ exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, "2016:01:28 09:17:34");
+ exif.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, "2016:01:28 09:17:34 UTC");
+ exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:28");
+ exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "09:14:00");
+ return exif;
+ }
+
+ private void assertParseDateTime(ExifInterface exif, ToLongFunction<ExifInterface> func) {
+ final int numOfThreads = 5;
+ final CountDownLatch latch = new CountDownLatch(numOfThreads);
+ final AtomicInteger count = new AtomicInteger(numOfThreads);
+
+ for (int i = 0; i < numOfThreads; i++) {
+ new Thread(() -> {
+ if (parseDateTime(exif, func)) count.decrementAndGet();
+ latch.countDown();
+ }).start();
+ }
+
+ try {
+ latch.await(10, TimeUnit.SECONDS);
+ } catch (InterruptedException ignored) {
+ }
+
+ assertEquals(0, count.get());
+ }
+
+ private boolean parseDateTime(ExifInterface exif, ToLongFunction<ExifInterface> func) {
+ final long expected = func.applyAsLong(exif);
+ try {
+ for (int i = 0; i < 1000; ++i) {
+ final long actual = func.applyAsLong(exif);
+ if (expected != actual) {
+ return false;
+ }
+ }
+ } catch (ArrayIndexOutOfBoundsException | NumberFormatException e) {
+ return false;
+ }
+ return true;
+ }
+}
+