0

I'm trying to hook my Android application up to Google Calendars. I've followed the quick start tutorial, have asked the user for permission to write to external storage, yet I cannot get the code to run without throwing a java.io.IOException.

My code looks like this:

GoogleCalendarModule.kt

class GoogleCalendarModule {
    var APPLICATION_NAME: String = "Conglobo"
    var JSON_FACTORY: JacksonFactory = JacksonFactory.getDefaultInstance()
    var TOKENS_DIRECTORY_PATH: String = "./tokens"

    var SCOPES: List<String> = Collections.singletonList(CalendarScopes.CALENDAR_READONLY)
    private var CREDENTIALS_FILE_PATH: String = "/credentials.json"

    fun getCredentials(HTTP_TRANSPORT: NetHttpTransport): com.google.api.client.auth.oauth2.Credential? {

        val inputStream: InputStream = this.javaClass.getResourceAsStream(CREDENTIALS_FILE_PATH)
            ?: throw FileNotFoundException("Resource Not found: $CREDENTIALS_FILE_PATH")

        val clientSecrets: GoogleClientSecrets = GoogleClientSecrets.load(JSON_FACTORY, InputStreamReader(inputStream))

        val tokenFolder = File(getExternalStorageDirectory(), File.separator.toString() + TOKENS_DIRECTORY_PATH)
        if (!tokenFolder.exists()) {
            tokenFolder.mkdirs()
        }

        val flow: GoogleAuthorizationCodeFlow = GoogleAuthorizationCodeFlow.Builder(
            HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, SCOPES)
            .setDataStoreFactory(FileDataStoreFactory(tokenFolder))
            .setAccessType("offline")
            .build();

        val receiver: LocalServerReceiver = LocalServerReceiver.Builder().setPort(8888).build()

        return AuthorizationCodeInstalledApp(flow, receiver).authorize("user")
    }

    fun doSomething() {
        val HTTP_TRANSPORT = NetHttpTransport()
        val service: Calendar = Calendar.Builder(HTTP_TRANSPORT, JSON_FACTORY, getCredentials(HTTP_TRANSPORT))
            .setApplicationName(APPLICATION_NAME)
            .build()

        val now = DateTime(System.currentTimeMillis())
        val events: Events = service.events().list("primary")
            .setMaxResults(10)
            .setTimeMin(now)
            .setOrderBy("startTime")
            .setSingleEvents(true)
            .execute()

        val items: List<Event> = events.items
        if (items.isEmpty()) {
            println("No events")
        } else {
            println("Upcoming events")
            for(event: Event in items) {
                var start: DateTime = event.start.dateTime
                if(start == null) {
                    start = event.start.date
                }

                print(event.summary)
            }
        }
    }
}

Then I check against current permissions in the MainActivity

MainActivity.kt

class MainActivity: AppCompatActivity(), ITeamFragmentDelegate {

    @Inject
    lateinit var teamInfoModule: TeamInfoModule;

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
                == PackageManager.PERMISSION_GRANTED) {
                print("Permission is granted");
            } else {

                print("Permission is revoked");
                ActivityCompat.requestPermissions(this, Array<String>(1) { Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
            }
        }


        GoogleCalendarModule().doSomething()

        setContentView(R.layout.activity_main)
        DaggerServiceModuleComponent.create().inject(this)

        val teamArrayList: ArrayList<Team> = this.teamInfoModule.getAllTeamData()

        for(team: Team in teamArrayList) {
            val bundle = Bundle()
            val teamFragment = TeamFragment()

            bundle.putParcelable("teamData", team)
            teamFragment.arguments = bundle

            supportFragmentManager.beginTransaction()
                .add(R.id.root_container, teamFragment)
                .commitAllowingStateLoss()
        }
    }

    override fun onTeamClicked(fragment: TeamFragment, team: Team) {
        val intent = Intent(this, ViewTeamBacklogActivity::class.java)
        intent.putExtra("teamId", team.id)
        startActivity(intent)
    }
}

And I even have the permissions declared in AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.bluelightlite">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".ViewTeamBacklogActivity">

        </activity>
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

I've searched around S.O and it appears to be a common problem but none of the solutions I've found have worked. Could someone please help?

Marc Freeman
  • 713
  • 2
  • 7
  • 30

3 Answers3

0

I don't know if it's useful, but you can try set your targetSdkVersion Less than 23 ?

igarasi
  • 137
  • 7
  • I gave it a try but it made everything worse, the application won't build at all using SDK 22. I'm currently on SDK 29 – Marc Freeman Feb 14 '20 at 13:38
  • https://stackoverflow.com/questions/31399122/how-to-access-storage-emulated-0 – igarasi Feb 14 '20 at 13:48
  • still hasn't helped I'm afraid. Still getting the error java.io.IOException: unable to create directory: /storage/emulated/0/tokens Permissions are definitely there though. This is strange – Marc Freeman Feb 14 '20 at 14:05
0

Found the problem.

As of Android SDK 23 and onwards (I believe). Refactoring GoogleCalendarModule.kt to this:

package com.example.bluelightlite.modules

import android.content.Context
import com.example.bluelightlite.constants.APPLICATION_NAME
import com.example.bluelightlite.constants.JSON_FACTORY
import com.google.api.client.http.javanet.NetHttpTransport
import com.google.api.client.util.DateTime
import com.google.api.services.calendar.Calendar
import com.google.api.services.calendar.model.Event
import com.google.api.services.calendar.model.Events

class GoogleCalendarsServiceModule constructor(context: Context) {

    private val googleCredentialsUtilityModule = GoogleCredentialsUtilityModule(context)

    fun getEvents(): List<Event> {

        val httpTransport = NetHttpTransport()
        val service: Calendar = Calendar.Builder(httpTransport, JSON_FACTORY, this.googleCredentialsUtilityModule.getCredentials(httpTransport))
            .setApplicationName(APPLICATION_NAME)
            .build()

        val now = DateTime(System.currentTimeMillis())
        val events: Events = service.events().list("primary")
            .setMaxResults(10)
            .setTimeMin(now)
            .setOrderBy("startTime")
            .setSingleEvents(true)
            .execute()

        return events.items;
    }
}

And creating a GoogleCredentialsUtilityModule.kt file that looks like this:

package com.example.bluelightlite.modules

import android.content.Context
import com.example.bluelightlite.constants.CREDENTIALS_FILE_PATH
import com.example.bluelightlite.constants.JSON_FACTORY
import com.example.bluelightlite.constants.SCOPES
import com.example.bluelightlite.constants.TOKENS_DIRECTORY_PATH
import com.google.api.client.auth.oauth2.Credential
import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp
import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets
import com.google.api.client.http.javanet.NetHttpTransport
import com.google.api.client.util.store.FileDataStoreFactory
import java.io.File
import java.io.FileNotFoundException
import java.io.InputStream
import java.io.InputStreamReader

class GoogleCredentialsUtilityModule constructor(private val context: Context) {

    /**
     * Creates an authorized Credential object.
     * @param HTTP_TRANSPORT The network HTTP Transport.
     * @return An authorized Credential object.
     * @throws java.io.IOException If the credentials.json file cannot be found.
     */
    fun getCredentials(HTTP_TRANSPORT: NetHttpTransport): Credential {

        val inputStream: InputStream = getCredentialsAsInputStream()
        val clientSecrets: GoogleClientSecrets = GoogleClientSecrets.load(JSON_FACTORY, InputStreamReader(inputStream))

        createTokenFolderIfMissing()

        val authorisationFlow: GoogleAuthorizationCodeFlow = getAuthorisationFlow(HTTP_TRANSPORT, clientSecrets)
        val receiver: LocalServerReceiver = LocalServerReceiver.Builder().setPort(8888).build()

        return AuthorizationCodeInstalledApp(authorisationFlow, receiver).authorize("freeman.marc880@gmail.com")
    }

    private fun getCredentialsAsInputStream(): InputStream {
        return this.javaClass.getResourceAsStream(CREDENTIALS_FILE_PATH)
            ?: throw FileNotFoundException("Resource Not found: $CREDENTIALS_FILE_PATH")
    }

    private fun createTokenFolderIfMissing() {
        val tokenFolder = getTokenFolder()
        if (!tokenFolder.exists()) {
            tokenFolder.mkdir()
        }
    }

    private fun getTokenFolder(): File {
        return File(this.context.getExternalFilesDir("")?.absolutePath + TOKENS_DIRECTORY_PATH)
    }

    private fun getAuthorisationFlow(HTTP_TRANSPORT: NetHttpTransport, clientSecrets: GoogleClientSecrets): GoogleAuthorizationCodeFlow {
        return GoogleAuthorizationCodeFlow.Builder(
            HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, SCOPES)
            .setDataStoreFactory(FileDataStoreFactory(getTokenFolder()))
            .setAccessType("offline")
            .build()
    }
}

makes it work.

What I did was pass the context down into the Google Credentials Module and asked it to get the external storage directory from the current context on this line of code:

  private fun getTokenFolder(): File {
        return File(this.context.getExternalFilesDir("")?.absolutePath + TOKENS_DIRECTORY_PATH)
    }
Marc Freeman
  • 713
  • 2
  • 7
  • 30
  • I get the `error java.lang.ClassNotFoundException: Didn't find class "com.sun.net.httpserver.HttpServer"`, do you have any hint? – Luca Murra Oct 24 '22 at 16:24
0

Add on AndroidManifest.xml

After go to Android Device: Settings -> Apps -> Advanced -> Permission Manager -> Files and Media -> YourApp -> Allow Management of All Files

Test your code again