Scoped storage came on the scene when Android 10 officially launched on September 3, 2019. It changed things a bit. As the name suggests, it ...
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Shape
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
Group
OurBiggestBlackFridaySale—Ever!
Introducingunlimitedaccesstoallcourses,allbooks,andournewmonthlyliveprofessionaldevelopmentseries!Just$899$399peryearduringourBlackFridayevent
Getitnow
raywenderlich.com
raywenderlich.com
Hidecontents
ScopedStorageinAndroid10:GettingStarted
GettingStarted
WhatIsScopedStorage?
WhyDoYouNeedScopedStorage?
ImplementingNon-ScopedStorageinAndroid10
AddingtheRequiredPermissions
FetchingImagesUsingMediaStore
DeletinganImageFromMediaStore
ListeningforChangesWithContentObserver
RegisteringtheContentObserver
UnregisteringtheContentObserver
WheretoGoFromHere?
WorkingwiththefilesystemisanimportantpartofdevelopinganyAndroidapp.UpuntilAndroid10,whenyougaveanappstoragepermission,itcouldaccessanyfileonthedevice.However,mostappsdon’tneedaccesstothewholestoragesystem.Theyusuallyperformtasksonasinglefileorasmallgroupoffiles.Thiscreatedathreattouserprivacy.
InAndroid10,Googleintroducedtheconceptofscopedstorage,whichenhancesusercontrolandprivacywhilecuttingbackthefileclutterthatremovedappsleavebehind.
Inthistutorial,you’llbuildasimplegalleryappcalledScopeo,whichdisplaysthephotosfromthedevice’ssharedstorage.Alongtheway,you’lllearnabout:
Whatscopedstorageisandwhyyouneedit.
Addingthecorrectpermissionswhenworkingwithfiles.
UsingMediaStoreAPIstoaccessthefiles.
Howtooptoutofscopedstorage,ifrequired.
DeletingfilesinMediaStoredirectlybyusinganIntentSender.
Note:Thistutorialassumesyou’refamiliarwiththebasicsofAndroiddevelopmentinKotlin.Ifyou’recompletelynewtoKotlinandAndroid,checkoutthisKotlintutorialandthisBeginningAndroidDevelopmenttutorial.
YoushouldalsohavesomefamiliaritywithGoogle’sarchitecturecomponents,suchasLiveDataandViewModel.GothroughAndroidJetpackArchitectureComponents:GettingStartedtolearnmoreaboutthem.
GettingStarted
ClicktheDownloadMaterialsbuttonatthetoporbottomofthepagetodownloadthestarterandfinalprojects.
OpenAndroidStudio3.6.1orlaterandchooseOpenanexistingAndroidStudioproject.ThennavigatetothestarterprojectdirectoryinsidethemainfolderandclickOpen.WaitforGradletosyncsuccessfullyandtakesometimetofamiliarizeyourselfwiththecode.
Asyouseefromthescreenshotabove,thestarterprojectcontainsthefollowingfiles:
Image.ktisasimpledataclassthatcontainssomepropertiesoftheimage.ItalsoincludesaDiffCallbacktoefficientlyupdatetheimagelistwhentheuserdeletesanimage.
MainActivity.ktiswherealltheUIinteractionsoccur.Itcontainsalotofboilerplatecodealreadyimplementedforyou.Forsimplicity,italsocontainsGalleryAdapterandImageViewHolderinthesamefile.
MainActivityViewModel.ktcontainsallthebusinesslogicoftheapp.You’lladdcodeinthisclasstoperformtime-consumingoperationsinthebackground.You’llobservethechangesusingLiveData.Allthebackgroundtasksusetherecommendedapproach,Kotlincoroutines,whichworkwellwithViewModel.
Note:IfyouwanttoknowmoreaboutDiffUtil.Callback,lookatAndroid’scallbackdocumentationortheofficialKotlincoroutinesdocumentation.
Nowthatyou’velookedoverthecodebase,buildandrun.You’llseethefollowingscreen:
OPENALBUMdoesn’tdoanythingrightnow.Asyouprogressthroughthetutorial,you’llbuildacompletelyfunctionalgalleryapp.
WhatIsScopedStorage?
TheStorageAccessFramework(SAF),introducedinAndroid4.4,madeiteasyfordeveloperstobrowseandopenfilesonthedevice.IntentactionslikeACTION_CREATE_DOCUMENT,ACTION_OPEN_DOCUMENTandACTION_OPEN_DOCUMENT_TREEperformtherequiredoperations.
Althoughitworks,SAFisslowandhighlyunpopularamongthedevelopercommunity.
Note:TakealookatAndroid’sofficialStorageAccessFrameworkguidetolearnmore.
ScopedstoragecameonthescenewhenAndroid10officiallylaunchedonSeptember3,2019.Itchangedthingsabit.Asthenamesuggests,itprovidesscoped—orlimited—accesstothefilesystem.Appsthatusescopedstoragehaveaccessonlytotheirappdirectoryonexternalstorageplusanymediatheappcreated.
Imagineyou’recreatingavoicerecorderapp.IfyouimplementscopedstorageinyourappforAndroid10andabove,you’llhavealimitedscopeforreadingandwritingfiles.Sinceyouraudiofilesresideintheappdirectory,youdon’tneedpermissiontoaccessormodifythem.
WhyDoYouNeedScopedStorage?
Therearethreemainreasonsforusingscopedstorage:
Improvingsecurity:Developersshouldhavecontroloverthefilestheirappscreate.Scopedstorageprovidesthiscontrol,lettingdevelopersworkwithfileswithouthavingtorequeststoragepermissionsAtthesametime,scopedstoragealsoprovidesbetterappdataprotectionbecauseotherappscan’taccessthesefileseasily.
Reducingleftoverappdata:Whenauseruninstallsanappfromtheirdevice,someappdatastillremainsinthesharedstorage.Withscopedstorage,thisproblemdisappearsasalltheappdataresidesexclusivelyintheappdirectory.
LimitingtheabuseofREAD_EXTERNAL_STORAGEpermission:Developershaveabusedthispermissionheavilybecauseitgavethemaccesstotheentireexternalstorage.Usingscopedstorage,appscanaccessonlytheirownfiles,foldersandothermediafiletypesusingstorageAPIs.
Eventhoughscopedstorageisuseful,theremaybetimeswhenyoudon’twanttouseit.Luckily,youcanstilloptoutofusingitinAndroid10.
ImplementingNon-ScopedStorageinAndroid10
AneasywaytooptoutofscopedstorageistosetrequestLegacyExternalStorageinyourapplicationinAndroidManifest.xmltotrue.
ThisattributeisfalsebydefaultonappstargetingAndroid10andhigher.Ifyousetittotrue,itwillmakeanexceptionforyourapp,allowingyoutouselegacystoragesolutions.Thisgrantsaccesstodifferentdirectoriesandmediafileswithoutanyissues.
Thisflaggivesdevelopersmoretimetotesttheirappsbeforemigratingtoscopedstorage.However,thisisn’trecommendedbecause,fromAndroid11onwards,thisattributewillnolongerbeavailable.
Note:Youwon’tusethisoptioninthistutorial.Ifyou’reeagertoseehowtosetitproperly,lookattheofficialdatastoragedocumentation.
Now,it’stimetogetstartedwithusingscopedstorageinanactualapp.
AddingtheRequiredPermissions
Foryourfirststep,you’lladdpermissionstoletyourappaccessandmodifyimagesfromotherapps.
OpenAndroidManifest.xmlandpastethefollowingcodejustbelowtheTODO:
Here,youuseREAD_EXTERNAL_STORAGEpermissiontoaccessimagestakenbyotherapps.WRITE_EXTERNAL_STORAGEpermissionletsyoudeletetheseimages.Yousetandroid:maxSdkVersionto28becauseinAndroid10andabove,youdon’tneedthispermissionanymore.
Laterinthetutorial,you’llexplicitlyasktheuser’spermissiontohandleimagedeletioninAndroid10.
Next,openMainActivity.ktandaddthefollowingpermissionsinsiderequestPermissions():
if(!haveStoragePermission()){
valpermissions=arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
ActivityCompat.requestPermissions(
//1
this,
//2
permissions,
//3
READ_EXTERNAL_STORAGE_REQUEST
)
}
Thecodeaboverequestsruntimepermissions,whicharemandatoryfromAndroid6.0andhigher.
requestPermissions()—fromthesupportlibrary—takesthreeparameters:
ThereferenceoftheActivityrequestingpermissions.
Astringarrayoftherequiredpermissions.
TherequestCode,whichmustbeuniquesinceonRequestPermissionsResult()usesthissamecodetohandlevarioususeractions.
Buildandrun.Now,tapOPENALBUM,whichshowsadialogaskingtheuserforstoragepermission:
Note:ThedialogdesignmaychangedependingontheAndroidversionyou’reusing.You’llseeadialogliketheoneshownaboveonanAndroid10deviceoremulator.
TapDenyandtheappwillshowtherationaleforthepermission.
Now,whenyoutapGRANTPERMISSIONandthentapAllow,itwillshowanemptyscreenasbelow:
Thescreenisemptybecauseitdoesn’tcontainanyimagesyet.You’lladdthemnext!
FetchingImagesUsingMediaStore
OpenMainActivityViewModel.ktandaddthefollowinginsidequeryImages(),justafterthe//TODOcomment.
//1
valprojection=arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_TAKEN
)
//2
valselection="${MediaStore.Images.Media.DATE_TAKEN}>=?"
//3
valselectionArgs=arrayOf(
dateToTimestamp(day=1,month=1,year=2020).toString()
)
//4
valsortOrder="${MediaStore.Images.Media.DATE_TAKEN}DESC"
//5
getApplication().contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder
)?.use{cursor->
imageList=addImagesFromCursor(cursor)
}
AddanymissingimportsbypressingOption-EnteronMacorAlt-EnteronPC.
Here’sastep-by-stepbreakdown:
projection:Anarraythatcontainsalltheinformationyouneed.It’ssimilartotheSELECTclauseofanSQLstatement.
selection:SimilartotheWHEREclauseinSQL,thisletsyouspecifyanycondition.The?inthestatementisaplaceholderthatwillgetitsvaluefromselectionArgs.
selectionArgs:Anarraycontainingthevaluethatwillreplace?inthestatementstoredinselection.Inthiscase,you’rerequestingalltheimagesfromthisyear.dateToTimestamp()isautilityfunctionthatacceptsaday,amonthandayear.Itreturnsthecorrespondingtimestampvalue,whichselectionArgsrequires.
sortOrder:Asthenamesuggests,thiscontainstheordertoreturntheimages.Thedefaultorderisascending,buthereyouaddtheDESCkeywordafterthevariablenametoswitchtodescendingorder.
query():AmethodofContentResolverthattakesinalltheaboveasparametersaswellasanadditionalUriparameterthatmapstotherequiredtableintheprovider.Inthiscase,therequiredUriisEXTERNAL_CONTENT_URIsinceyouarerequestingimagesfromoutsidetheapp.Uriisalwaysmandatory.Hence,itisanon-nullableparameterwhiletherestoftheparametersarenullable.
Phew!Thatwasalottotakein.Keepitup.
Buildandruntoseewhatyou’veachievedsofar.Assumingyougrantedthepermissionearlier,you’llnowseesomephotosinsteadoftheblankscreen:
Theimageswillbedifferentonyourdevice.Ifyouhaven’ttakenapictureintheyear2020,thenthescreenwillstillbeblank.Inthatcase,goaheadandtakeaselfiewithyourcatordog!Whenyouopentheappagain,you’llseeyourpicturethere.Tapanyimageinthegridanditwillshowadeletedialog.
TappingtheDELETEbuttonwon’tdoanythingyet.Getreadytodeleteanimage!
DeletinganImageFromMediaStore
JumptoperformDeleteImage()insideMainActivityViewModel.ktandaddthefollowingcode:
try{
//1
getApplication().contentResolver.delete(
image.contentUri,"${MediaStore.Images.Media._ID}=?",
arrayOf(image.id.toString())
)
}
//2
catch(securityException:SecurityException){
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.Q){
valrecoverableSecurityException=
securityExceptionas?RecoverableSecurityException
?:throwsecurityException
pendingDeleteImage=image
_permissionNeededForDelete.postValue(
recoverableSecurityException.userAction.actionIntent.intentSender
)
}else{
throwsecurityException
}
}
AddanymissingimportsbypressingOption-EnteronMacorAlt-EnteronPC.
Thereareafewimportantthingsyoushouldknowaboutthisblockofcode:
Here,youcallcontentResolver.delete()insideatryblocksincethismethodcanthrowaSecurityExceptionatruntime.ThemethodrequirestheContentUrioftheimageyouwanttodelete.Inthewhereparameter,youspecifythatyouwanttodeleteanimagebasedonits_ID.Inthefinalparameter,youpassthestringversionofthe_IDinanarray.
InAndroid10andabove,itisn’tpossibletodeleteormodifyitemsfromMediaStoredirectly.Youneedpermissionfortheseactions.ThecorrectapproachistofirstcatchRecoverableSecurityException,whichcontainsanintentSenderthatcanprompttheusertograntpermission.YoupassintentSendertotheactivitybycallingpostValue()onyourMutableLiveData.
Note:YoucanlearnmoreaboutMutableLiveDataandpostValue()fromAndroid’sMutableLiveDatadocumentation.
Now,gotoMainActivity.ktandaddthefollowingcodetoviewModel.permissionNeededForDelete.observe(),justafterthe//TODOcomment.
intentSender?.let{
startIntentSenderForResult(
intentSender,
DELETE_PERMISSION_REQUEST,
null,
0,
0,
0,
null
)
}
startIntentSenderForResult()launchesintentSender,whichyoupassedtoit.DELETE_PERMISSION_REQUESTisauniquerequestcodeusedtoidentifyandhandletheactionwhentherequestcompletes.
Beforeyoutrythenewdeletefeature,Scopeoneedsafewmorefinishingtouches!
ListeningforChangesWithContentObserver
ContentObserverisaclassthatlistensforchangeswheneverthedatainthecontentproviderchanges.Sincedatawillchangewheneveryoudeleteanyimageintheapp,youneedtouseaContentObserver.
RegisteringtheContentObserver
StartbyregisteringtheContentObserver.
OpenMainActivityViewModel.ktandaddthefollowingcodeinsideloadImages(),justafterthe//TODOcomment.
contentObserver=getApplication().contentResolver.registerObserver(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
){
loadImages()
}
ThecodeabovejustcallstheextensionmethodContentResolver.registerObserver(uri:Uri,observer:(selfChange:Boolean)->Unit),whichisalreadyimplementedasshownbelow:
/**
*Extensionmethodtoregistera[ContentObserver]
*/
privatefunContentResolver.registerObserver(
uri:Uri,
observer:(selfChange:Boolean)->Unit
):ContentObserver{
//1
valcontentObserver=object:ContentObserver(Handler()){
overridefunonChange(selfChange:Boolean){
observer(selfChange)
}
}
//2
registerContentObserver(uri,true,contentObserver)
returncontentObserver
}
Lookcloselyatthecode,andyou’llnoticetwothingsarehappening:
contentObserveroverridesonChange().Thismethoddefineswhathappensifthedataintheproviderchanges.Inthiscase,itwillcallloadImages()passedasalambda.AbestpracticeistoalwaysuseaHandler()whencreatingContentObserver.
Next,theextensionmethodregisterstheContentObserverusingtheuripassedtoit.ThesecondparameterpassedastrueindicatesthatalltheotherdescendantURIs,startingwiththegivenURI,shouldtriggerthemethodcall.ThefinalparameteristheinstanceoftheContentObserveryoucreatedearlier.
Nowthatyou’velearnedhowtoregistertheContentObserver,takeamomenttofindouthowandwhytounregisteritagain.
UnregisteringtheContentObserver
BeingagoodAndroidcitizen,youshouldalsounregisteryourContentObsersertopreventmemoryleaks.AddthefollowingcodetoonCleared()insideMainActivityViewModel.kt.
contentObserver?.let{
getApplication().contentResolver.unregisterContentObserver(it)
}
ThiscodejustcallsunregisterContentObserver()oftheContentResolver.ViewModelcallsonCleared()whenit’snotusedanymore,soit’stheperfectplacetounregister.
Well,it’sfinallydone!Goaheadandrunyourapptocheckoutthedeletefeature.IfyoutrydeletinganimageonanAndroid10device,itwillnowaskforpermission:
Thedialogwon’tshowondevicesrunningolderversionsofAndroidsincescopedstorageisonlyavailableondevicesrunningAndroid10orabove.
Ifyoudenypermission,nothinghappens.Butifyougrantpermission,you’lldeletetheimagepermanentlyandtheappwillloadtheupdatedlist.
WheretoGoFromHere?
DownloadthecompletedprojectfilesbyclickingtheDownloadMaterialsbuttonatthetoporbottomofthetutorial.
Inthistutorial,youlearnedaboutscopedstorageandworkedonthemostpopularusecasewhereitapplies.Ifyouwanttolearnaboutotherusecases,checkoutthisvideofromAndroidDevelopersSummit2019.
ThisisagreattimetogetyourappsworkingwithscopedstorageasitwillbemandatoryforallnewappsinAugust2020andeveryapptargetingAndroid11.
ManynewscopedstoragechangeswillcomeintoeffectwithAndroid11.Checkthemouthere:StorageUpdatesinAndroid11.
Also,takealookatDataPrivacyforAndroidtolearnmoreaboutuserprivacyandsecurity.
Ihopeyoulikedthistutorial.Ifyouhaveanyquestionsorcomments,pleasejointheforumdiscussionbelow.
CoreConcepts
Android&KotlinTutorials
DownloadMaterials
raywenderlich.comWeekly
Theraywenderlich.comnewsletteristheeasiestwaytostayup-to-dateoneverythingyouneedtoknowasamobiledeveloper.
Signupnow
Website
Getaweeklydigestofourtutorialsandcourses,andreceiveafreein-depthemailcourseasabonus!
OurBiggestBlackFridaySale—Ever!
Introducingunlimitedaccesstoallvideocourses,allbooks,andournewmonthlyliveprofessionaldevelopmentseries!Just$899$399peryearduringourBlackFridayevent.
Getitnow
Morelikethis
MarkComplete(AllChapters)
ClearProgress(AllChapters)
MarkComplete
ClearProgress
Completed
New
New
ComposeforDesktop:GetYourWeather!
Android&Kotlin
GettingStarted
Nov222021·Article(25mins)
BuildadesktopweatherappwithComposeforDesktop!You’llgetuserinput,fetchnetworkdataanddisplayitallwiththeComposeUItoolkit.
Nov222021·Article(25mins)
Completed
BuildadesktopweatherappwithComposeforDesktop!You’llgetuserinput,fetchnetworkdataanddisplayitallwiththeComposeUItoolkit.
Nov222021·Article(25mins)
Completed
MarkComplete(AllChapters)
ClearProgress(AllChapters)
MarkComplete
ClearProgress
Completed
New
New
BlackFridaySale:ProSubscriptions+MonthlyProSeminarsfor$399
iOS&Swift
Announcements
Nov222021·Article(10mins)
AnnouncingournewProfessionalDevelopmentSeminars,partofourBlackFriday2021event!
Nov222021·Article(10mins)
Completed
AnnouncingournewProfessionalDevelopmentSeminars,partofourBlackFriday2021event!
Nov222021·Article(10mins)
Completed
MarkComplete(AllChapters)
ClearProgress(AllChapters)
MarkComplete
ClearProgress
Completed
New
New
iPadOS15Tutorial:What’sNewforDevelopers
iOS&Swift
OtherCoreAPIs
Nov222021·Article(30mins)
Seewhat’snewiniPadOS15andtakeyourapptothenextlevelwithgroundbreakingchanges!
Nov222021·Article(30mins)
Completed
Seewhat’snewiniPadOS15andtakeyourapptothenextlevelwithgroundbreakingchanges!
Nov222021·Article(30mins)
Completed
MarkComplete(AllChapters)
ClearProgress(AllChapters)
MarkComplete
ClearProgress
Completed
New
New
BlackFridaySaleComingSoon!
iOS&Swift
Announcements
Nov192021·Article(2mins)
Ourbiggest-everBlackFridayandCyberMondaysalelaunchesnextweek.Don’tmissit!
Nov192021·Article(2mins)
Completed
Ourbiggest-everBlackFridayandCyberMondaysalelaunchesnextweek.Don’tmissit!
Nov192021·Article(2mins)
Completed
Contributors
AnshdeepSingh
AnshdeepisaGoogleCertifiedAndroidDeveloperbasedinVancouver,Canada.Heispassionateaboutbuildinggreatuser-centric...
Author
FabioBombardi
FabioisanItaliandeveloper.Hehasbeenspendingmanyhoursofhislifeinfrontofamonitorsincehewaseightyearsold...
TechEditor
SandraGrauschopf
SandraGrauschopfistheBookTeamLeadforraywenderlich.com.She'sanexperiencedwriter,editor,andcontentmanageraswell...
Editor
JuliaZinchenko
GraphicdesignerandillustratorbasedinUkraine
Illustrator
AldoOlivares
MobileDeveloperpassionateaboutcreatingamazingappswithgreatuserinterfaces.Founderofaldominium.com,company...
Fpe
EricSoto
EricisAndroidTutorialTeamLeadandaProfessionalSoftwareEngineerandcertifiedAgile-ScrumMasterfocusingonAppleiOS...
TeamLead
Inthistutorial,you’lllearnhowtousescopedstorageinyourAndroid10appbybuildingasimpleimagegallery.
Comments
ShowComments.
OurBiggestBlackFridaySale—Ever!
Introducingunlimitedaccesstoallvideocourses,allbooks,andournewmonthlyliveprofessionaldevelopmentseries!Just$899$399peryearduringourBlackFridayevent.
Getitnow