Android Code Snippet [updated] GoogleDrive via REST API V3 - Small Testproject

Updated Oct 30, 2023
Attached is a small project (version 19) to get a grip on Google Drive.

Most work was done by mw71, but this brings it all together from scattered informations to a usable entrypoint.
' ~~~-- ~~~-- ~~~-- ~~~-- ~~~-- ~~~-- ~~~-- ~~~-- ~~~--​
' Previous Info sources on B4X (sorted by date DEC)​
' ~~~-- ~~~-- ~~~-- ~~~-- ~~~-- ~~~-- ~~~-- ~~~-- ~~~--​
' ~~~-- ~~~-- ~~~-- ~~~-- ~~~-- ~~~-- ~~~-- ~~~-- ~~~--
' 2023-10-28, JohnC, changes in libs and modules --> https://www.b4x.com/android/forum/t...st-api-v3-small-testproject.95778/post-964342


Libraries:​
30-10-2023_09-34-07.png

Modules:​
30-10-2023_09-34-36.png
Manifest:​
B4X:
'This code will be applied to the manifest file during compilation.

'You do not need to modify it in most cases.

'See this link for for more information: https://www.b4x.com/forum/showthread.php?p=78136

AddManifestText(

<uses-sdk android:minSdkVersion="20" android:targetSdkVersion="33"/>

<supports-screens android:largeScreens="true"

    android:normalScreens="true"

    android:smallScreens="true"

    android:anyDensity="true"/>)

SetApplicationAttribute(android:icon, "@drawable/icon")

SetApplicationAttribute(android:label, "$LABEL$")

'End of default text.

AddActivityText(Main,

  <intent-filter>

        <action android:name="android.intent.action.VIEW" />

        <category android:name="android.intent.category.DEFAULT" />

        <category android:name="android.intent.category.BROWSABLE" />

        <data android:scheme="$PACKAGE$" />

    </intent-filter>

    )

Notes:​
1. The module GoogleOauth2 contains "Sub Testconnect" from mw71
2. Read and understand the project specific setup below​
Create a new project in the Google AP developer console
04-08-_2018_13-05-22.jpg

Select "Google Drive API" in the dashboard​
04-08-_2018_13-09-40.jpg

Enable the API​
04-08-_2018_13-11-49.jpg

Create credentials​
04-08-_2018_13-14-25.jpg
As alerted by (1) on first entry you may create the credentials via button (2) right now. It is recommended to create credentials via button (3)​
04-08-_2018_13-55-29.jpg 04-08-_2018_13-55-36.jpg 04-08-_2018_13-55-49.jpg
You need to fill in "SHA-1" and "Package name". Both are available in your B4A IDE:​
04-08-_2018_13-29-14.jpg 04-08-_2018_13-34-38.jpg


Then receive your client ID:​
04-08-_2018_13-51-02.jpg


...then scroll down to the "Advanced Settings" and check-mark the "Enable Custom URI Scheme" (then it says it may take 5 mins to a few hours for the change to take effect). @John

EnableCustomURI.png


The program sequence is as follows:​
1. "GoogleDrive" is initialized in Main with the "ClientId" for Oauth2​
1.1 "GoogleDrive" initializes "GoogleOauth2" and informs about the "Scope= ....drive"​

2. Main calls "ConnectToDrive" and waits for message from "GD_Connected".​
2.1 "GoogleDrive" requests an AccessToken via "GoogleOauth2"​
2.2 "GoogleDrive" executes a "TestConnect" and reports via "GD_Connected" to waiting Main​

3. Main displays the AccessToken in Label1​

With the AccessToken several actions can be can performed with the drive:​
If you need a "ClientSecret" at some point create a new OAuth2 ClientId with the ApplicationType "Web application"

B4X:
' ------------------------------------------------------------------------------
Sub btnStart_Click As ResumableSub
   Log("#-btnStart_Click -----------------------------------")
   Label1.Text = "(Trying to connect to Google Drive...)"
   GD.Initialize(Me, "GD", ClientIdOauth, ClientSecret) ', AppApiKey)
   GD.ConnectToDrive
   wait for GD_Connected(mapRet As Map)
   ' ww~~-- ww~~-- ────────────────────
   '
   'Log("#-  x129, GD_Connected, mapRet=" & mapToPrettyString(mapRet) )
   Label1.Text = "Access_token= " & mapRet.GetDefault("access_token", "?")
   '
   GD.ShowFileList("")
   wait for GD_FileListResult(lstFiles As List)
   ' ww~~-- ww~~-- ────────────────────
   '
   Log("#-GD_FileListResult, lstFiles=" & lstToPrettyString(lstFiles) )
   ListView1.Clear
   Dim lblX As Label
   lblX = ListView1.SingleLineLayout.Label
   lblX.TextSize = 12
   lblX.TextColor = Colors.Green
   For i=0 To lstFiles.Size -1
       ListView1.AddSingleLine(lstFiles.Get(i))
   Next
   '
   btnCreaFolder.Enabled = True
   btnUpload.Enabled = True
   btnDownload.Enabled = True
   btnSearch.Enabled = True
   '
   Return Null
End Sub
' ------------------------------------------------------------------------------
Sub btnCreaFolder_Click As ResumableSub
   Log("#-")
   Log("#- ---*** ---*** ---*** ---*** ---*** ---*** ---*** ---*** ")
   Log("#-Sub btnCreaFolder_Click")
   Dim strFolderParentID As String = ""
   edtFolderToCreate.Text = "Testfolder_01"
   Label3.Text = $"(Trying to create folder ${edtFolderToCreate.Text})"$
   GD.CreateFolder(edtFolderToCreate.Text, strFolderParentID)
   wait for GD_FolderCreated(strFileId As String)
   ' ww~~-- ww~~-- ────────────────────
   '
   Label3.Text = "FileId= " & strFileId
   Log("#-  x178, strFileId=" & strFileId)
   Return Null
End Sub
' ------------------------------------------------------------------------------
Sub btnUpload_Click As ResumableSub
   Log("#-")
   Log("#- ---*** ---*** ---*** ---*** ---*** ---*** ---*** ---*** ")
   Log("#-Sub btnUpload_Click")
   Dim strFolderParentID As String = ""
   Dim strFilenameInGdrive As String = "testdatafile4"
   EditText2.Text = strFilenameInGdrive
   Dim strFileToUpload As String     = "testdata2.json"
   Label2.Text = $"(Trying to upload ${strFileToUpload})"$
   GD.UploadFile("", File.DirAssets, strFileToUpload, strFolderParentID, strFilenameInGdrive)
   wait for GD_FileUploadDone(strFileId As String)
   ' ww~~-- ww~~-- ────────────────────
   '
   Label2.Text = "FileId= " & strFileId
   Log("#-  x130, strFileId=" & strFileId)

   edtFileToDownload.Text = strFileId

   Return Null
End Sub
' ------------------------------------------------------------------------------
Sub btnSearch_Click As ResumableSub
   Log("#-")
   Log("#- ---*** ---*** ---*** ---*** ---*** ---*** ---*** ---*** ")
   Log("#-Sub btnSearch_Click")
   Label4.Text = $"(Trying to search for ${EditText2.Text.Trim})"$
   GD.SearchForFileID(EditText2.Text.Trim, EditText1.Text.Trim)
   wait for GD_FileFound(strFileId As String)
   ' ww~~-- ww~~-- ────────────────────
   '
   Label4.Text = "FileId= " & strFileId
   Log("#-  x170, strFileId=" & strFileId)
   Return Null
End Sub
' ------------------------------------------------------------------------------
Sub btnDownload_Click As ResumableSub
   Log("#-")
   Log("#- ---*** ---*** ---*** ---*** ---*** ---*** ---*** ---*** ")
   Log("#-Sub btnDownload_Click")

   Label5.Text = $"(Trying to download ${edtFileToDownload.Text.Trim})"$
   GD.DownloadFile(edtFileToDownload.Text.Trim, File.DirDefaultExternal, "aaa_file_" & edtFileToDownload.Text.Trim)
   wait for GD_FileDownloaded(strRet As String)
   ' ww~~-- ww~~-- ────────────────────
   '
   Label5.Text = "strRet= " & strRet
   Log("#-  x203, strRet=" & strRet)

End Sub
' ------------------------------------------------------------------------------
During the first run you might see a warning if the app hasn't been verified:

30-10-2023_09-29-13.png


Just click "Advanced" and "Go to grive..."

Now you're ready to test:

04-08-_2018_18-41-27.jpg


B4X:
'           Sub ConnectToDrive
'            Sub ShowFileList(ParentFolderID As String)
'            Sub CreateFolder(FolderName As String, ParentFolderID As String)
'            Sub UploadFile(FileId As String, LocalPath As String, LocalFilename As String, ParentFolderID As String, Name As String)
'            Sub DownloadFile(FileID As String, LocalPath As String, LocalFilename As String)
'            Sub SearchForFileID(SearchFile As String, ParentFolderID As String)
'            Sub SearchForFolderID(SearchFolder As String, ParentFolderID As String)

If something is fundamentally wrong, please explain and enclose a corresponding test project.

Questions should be asked in the Android questions forum including errormessages or code in code-tags.

Edit: Testproject updated to 19 due to module, lib and setupcondition bits.


Info: If you have trouble with apostrophes in the name of files or folders then this will help (thanks to @Dave O): https://www.b4x.com/android/forum/t...api-for-folder-names-with-apostrophes.130495/
 

Attachments

  • googledrivetest_19.zip
    19.6 KB · Views: 111
Last edited:

leitor79

Active Member
Licensed User
Longtime User
Hi!

Thank you very much for sharing! I'll start to see what can I do with this.

Meanwhile, I have a question: in the comments, there is a statement:

' #### NEVER STORE THIS IN PRODUCTION SOURCECODE #############################################################
(regarding the ClientIDOauth). So, what would be the alternative? storing it where?

Thank you very much!
 

leitor79

Active Member
Licensed User
Longtime User
Well, I've started the project, and when I click the "connect" button, Chrome opens and shows the screen below.

Besides this error I'd like to know: is it the end behavior that the browser will open when the user wants to store/use a google drive file?

Regards!
 

Attachments

  • 2018-08-23_21-26-41.png
    2018-08-23_21-26-41.png
    72.3 KB · Views: 602

fredo

Well-Known Member
Licensed User
Longtime User
...storing it where?

A potential attacker can see all possibilities of local storage with sufficient energy sooner or later with the means of reverse engineering that are common today.

Probably the safest method would be to keep the keys available on your own external server and to retrieve them encrypted with your own access code at short notice. However, you need the knowledge of how to do this exactly and a suitable server.

Since I am not a specialist in the field of access security, I currently only see these easy to handle possibilities for us to make a hacker's search as difficult and time-consuming as possible:
  • Obfuscate the keys as "clever altered strings" and save them encrypted in a keyvaluestore
  • Hide the keys via steganography in harmless looking images
  • Be careful on how the code looks around the use of the security measures. A msgbox("access denied",...) will certainly be quickly detected by a superficially interested 12-year-old between school and the next fortnite session.
Here are some reads around android security:
Some more information on securing android apps:
To check your own app for hidden/detectable strings:
 
Last edited:

leitor79

Active Member
Licensed User
Longtime User
A potential attacker can see all possibilities of local storage with sufficient energy sooner or later with the means of reverse engineering that are common today.

Probably the safest method would be to keep the keys available on your own external server and to retrieve them encrypted with your own access code at short notice. However, you need the knowledge of how to do this exactly and a suitable server.

Since I am not a specialist in the field of access security, I currently only see these easy to handle possibilities for us to make a hacker's search as difficult and time-consuming as possible:
  • Obfuscate the keys as "clever altered strings" and save them encrypted in a keyvaluestore
  • Hide the keys via steganography in harmless looking images
  • Be careful on how the code looks around the use of the security measures. A msgbox("access denied",...) will certainly be quickly detected by a superficially interested 12-year-old between school and the next fortnite session.
Here are some reads around android security:

Thank you very much, fredo! What a great post. I was aware about the reverse engineering vulnerability, but I thought that the "release (obfuscated)" mode was enough for that?
 

leitor79

Active Member
Licensed User
Longtime User
This is probably due to the incorrect creation of the API credentials or the entry in the manifest.

Make sure to have a working project of Erel's Oauth2 class.

Thanks again, fredo! I was running your example. I supposed the manifest was right, but "just checking" it I realized that your example has one package name and my credential was created with my package name, so that was it. I've changed your package name and put it mine and now it works great.

Thank you very much!
 

leitor79

Active Member
Licensed User
Longtime User
One more question; I don't really need access to the whole user's drive. The scope =
https://www.googleapis.com/auth/drive.appdata would fit better in my app.

Do I have to change just the Private scope As String in the GoogleDrive module, or do I have to have other considerations?

Thank you very much!
 

fredo

Well-Known Member
Licensed User
Longtime User

leitor79

Active Member
Licensed User
Longtime User
Hi fredo!

I've been looking at the service properties, and I have some questions.

First of all, I haven't found any issues regarding the change of scope. I've just changed:

B4X:
    'Private scope As String = "https://www.googleapis.com/auth/drive"
    Private scope As String = "https://www.googleapis.com/auth/drive.appdata"

...and everything seems to be working. I can upload a file and restore it.

The thing now is tat I'd like to show the user the last modified date/time of that file before retrieving it.

So, I've started trying to modify SearchFileID to include that information. Intercepting the H_SFI.GetString I notice that the date information is not coming in the response. THen I've googled and found this. However, I couldn't find the modifiedTIme property into the request. I think it would be in GoogleDrive.SearchForFileID, at H_SFI.Download2... line, something like this:

B4X:
        H_SFI.Download2("https://www.googleapis.com/drive/v3/files", _
        Array As String("access_token", myAccessToken, _
                             "corpora", "user", _
                             "q","mimeType!='application/vnd.google-apps.folder' and" & _
                             "name='" & SearchFile & "' and " & _
                             "trashed=false", _
                            "properties","modifiedTime"))

However, this doesn't provide the requested information.

Other ways ("fields" instead of "properties", por putting the stuff into the "q" parameter) returns an error.

Do you have any insight of this?

Thank you very much!
 

fredo

Well-Known Member
Licensed User
Longtime User
... any insight of this?

In order to get special information about a file BEFORE the download, it must be given as metadata during the upload. Since there is no "tag" field and the other fields are not suitable, only a workaround remains via the "description" field.

But be cautious. This is a weak solution, as the "description" may be changed on the GDrive by someone.​

In the GDrive class add a "description" field to the metadata
26-08-_2018_07-49-07.jpg

The "description" can contain not only human-readable information but also purely technical information in the form of strings of characters.
26-08-_2018_07-54-39.jpg
In this case, a time stamp in the form "#1534785367351" is appended.

However, the "description" can also be viewed and modified by the Gdrive user in a browser (except for the files in "files.appdata").
26-08-_2018_07-56-29.jpg
To evaluate the "description" as metadata, the "sub ShowFileList" must be adapted in the GDrive class module.
26-08-_2018_08-04-35.jpg
B4X:
Sub ShowFileList(ParentFolderID As String) As ResumableSub'ignore
    Log("#-GoogleDrive.Sub ShowFileList, ParentFolderID=" & ParentFolderID)
    Dim h_sfl As HttpJob
    h_sfl.Initialize("", Me)
    If ParentFolderID="" Then
        h_sfl.Download2("https://www.googleapis.com/drive/v3/files", _
             Array As String("access_token", myAccessToken, _
                             "corpora", "user", _
                             "fields", "files(description,appProperties,createdTime,id,kind,lastModifyingUser,mimeType,modifiedByMe,modifiedByMeTime,name,properties,size,starred,version)" , _
                             "q","mimeType!='application/vnd.google-apps.folder' and trashed=false"))
    Else
        h_sfl.Download2("https://www.googleapis.com/drive/v3/files", _
             Array As String("access_token", myAccessToken, _
                             "corpora", "user", _
                             "fields", "files(description,appProperties,createdTime,id,kind,lastModifyingUser,mimeType,modifiedByMe,modifiedByMeTime,name,properties,size,starred,version)" , _
                             "q","mimeType!='application/vnd.google-apps.folder' and '" & ParentFolderID & "' in parents and trashed=false"))
    End If
    '
    ' Find the parameters with API explorer:
    '     --> https://developers.google.com/apis-explorer/?hl=en_GB#p/drive/v3/drive.files.list?corpora=user&q=mimeType!%253D'application%252Fvnd.google-apps.folder'
    '                 files(appProperties,createdTime,fileExtension,id,kind,size)
 
    Wait For (h_sfl) JobDone(h_sfl As HttpJob)
    ' ww~~-- ww~~-- ────────────────────
    Log("#-  x207, h_sfl.Success=" & h_sfl.Success)
    '
    If h_sfl.Success Then
        Log("#-  x209, h_sfl.GetString=" & h_sfl.GetString)
        '    {
        '         "files": [
        '          {
        '           "kind": "drive#file",
        '           "id": "sdfsdf",
        '           "name": "A1 gesicherte Daten vom 21.8., 10:23",
        '           "mimeType": "application/x-zip",
        '           "description": "Datendatei von A1 Wissyxcyxc\nLetzte Änderung: 20.8.2018, 19:16\n\n#1534785367351",
        '           "starred": false,
        '           "version": "2",
        '           "createdTime": "2018-08-21T08:23:42.114Z",
        '           "modifiedByMeTime": "2018-08-21T08:23:42.114Z",
        '           "modifiedByMe": true,
        '           "lastModifyingUser": {
        '            "kind": "drive#user",
        '            "displayName": "sadasd sdasda sd",
        '            "photoLink": "https://lh3.goog.../photo.jpg",
        '            "me": true,
        '            "permissionId": "453453455345",
        '            "emailAddress": "sdfsfsdfs fghfhfgh"
        '           },
        '           "size": "311567"
        '          },
        '          {
        '           "kind": "drive#file",
        '           "id": "fdssdfsdf",
        '           "name": "A1 gesicherte Daten vom 21.8., 10:19",
        '           "mimeType": "application/x-zip",
        '           "description": "Datendatei von A1 Wissyxcyxc\nLetzte Änderung: 20.8.2018\n#1534785367351",
        '           "starred": false,
        '           "version": "2",
        '           "createdTime": "2018-08-21T08:19:30.441Z",
        '           "modifiedByMeTime": "2018-08-21T08:19:30.441Z",
        '           "modifiedByMe": true,
        '           "lastModifyingUser": {
        '            "kind": "drive#user",
        '            "displayName": "sadasd sdasda sd",
        '            "photoLink": "https://lh3.goog.../photo.jpg",
        '            "me": true,
        '            "permissionId": "453453455345",
        '            "emailAddress": "sdfsfsdfs fghfhfgh"
        '           },
        '           "size": "311567"
        '          }
        '         ]
        '        }
        '
        Dim files As List
        Dim Map1 As Map
        Dim J As JSONParser
        J.Initialize(h_sfl.GetString)
        files.Initialize
        Map1 = J.NextObject
        files = Map1.Get("files")
'        Log("#-  x255, files.Size=" & files.Size)
        '
        Dim lstFilesToDownload As List
        lstFilesToDownload.Initialize
        Dim mapFileEntry As Map
        For i = 0 To files.Size - 1
            mapFileEntry = files.Get(i)
            'Log("#-    x261, mapFileEntry=" & fcn.logm(mapFileEntry))
            Dim mapFileEntryExp As Map
            mapFileEntryExp.Initialize
            mapFileEntryExp.Put("id", mapFileEntry.Get("id"))
            mapFileEntryExp.Put("name", mapFileEntry.Get("name"))
            mapFileEntryExp.Put("mimeType", mapFileEntry.Get("mimeType"))
            mapFileEntryExp.Put("createdTime", mapFileEntry.Get("createdTime"))
            mapFileEntryExp.Put("description", mapFileEntry.Get("description"))
            lstFilesToDownload.Add(mapFileEntryExp)
        Next
'        Log("#-  x220, lstFilesToExport=" & fcn.loglst(lstFilesToExport, "    "))
        'CallSub2(evModule, evName & "_FileListResult", FilelisteExport)
    Else
        'hhgf-Log("#-  x231, h_sfl.ErrorMessage=" &  h_sfl.ErrorMessage)
    End If
    h_sfl.Release
    'hhgf-Log("#-        x233, end of ShowFileList >>>")
    'hhgf-Log("#-")
    'hhgf-Log("#-")
    Return lstFilesToDownload
 
End Sub

Now, for example, the received time stamp can be evaluated BEFORE a download and can be compared with a local value.
26-08-_2018_08-13-45.jpg
In this example, the attached time stamp is extracted and evaluated.
 
Last edited:

leitor79

Active Member
Licensed User
Longtime User
Hi Fredo! Thank you very much for your answer, again!

I've understood your explanation.

However, I don't use the ShowFileList method, since I'm writing on appdata. I've tried replacing the link at the SearchForFileID method to include only the properties I want, and it worked!


B4X:
        H_SFI.Download2("https://www.googleapis.com/drive/v3/files", _
        Array As String("access_token", myAccessToken, _
                             "corpora", "user", _
                             "fields", "files(id,name,modifiedTime)" , _
                             "q","mimeType!='application/vnd.google-apps.folder' and" & _
                             "name='" & SearchFile & "' and " & _
                             "trashed=false"))

So now, I got 3 properties back. Then, below, I use them this way:

B4X:
CallSub3(evModule, evName & "_FileFound", fileEntry.Get("id"), fileEntry.Get("modifiedTime"))

Just in case it would be useful for others, if you add the "fileds" property it seems that you should explicitly declare id and name (both things came by default if you don't use the field property, but if you add it and you don't add id and name, then you won't get those properties; only the ones you declare -modifiedTime only, in my case-)

Thank you very much for sharing this!
 

leitor79

Active Member
Licensed User
Longtime User
Ok. Thank you.

View attachment 71597
Could it be that a space is missing here? In the concatenation "... andname=" would be created...

Hi fredo!

That section was a copypaste of your demo project. However, it seems to work that way. I've added the missing space, and it works too... so, maybe google is prepared for missing spaces?

Regards!
 

fredo

Well-Known Member
Licensed User
Longtime User
was a copypaste

Thank you for noticing.

A new Testproject was uploaded to correct this:

29-08-_2018_06-36-06.jpg


Hmm, that's weird.

It would be impressive if the API recognizes such errors as typical and automatically corrects "andsomething" in "and something".

29-08-_2018_08-13-07.jpg
 

leitor79

Active Member
Licensed User
Longtime User
Thank you for noticing.
Actually it was YOU that noticed it! I've just pasted your code without noticing there was something wrong.

Thank you very much, again!

PS: Maybe the "andtrashed" part was completely ignored by the API and google was returning also trashed files? Because your previous code throw no exceptions and get results back.
 
Top