├── .gitignore ├── .idea ├── gradle.xml ├── misc.xml ├── modules.xml └── runConfigurations.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── libs │ └── androidauto.jar ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── eu │ │ └── vranckaert │ │ └── hello │ │ └── auto │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── eu │ │ │ └── vranckaert │ │ │ └── hello │ │ │ └── auto │ │ │ ├── HelloAndroidAutoActivity.java │ │ │ ├── HelloAndroidAutoActivityService.java │ │ │ └── LoginActivity.java │ └── res │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_hello_aa.xml │ │ └── activity_login.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ ├── ic_launcher_round.png │ │ └── waving_android.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── automotive_app_desc.xml │ └── test │ └── java │ └── eu │ └── vranckaert │ └── hello │ └── auto │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AndroidAutoHelloWorld 2 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 26 5 | buildToolsVersion "26.0.1" 6 | defaultConfig { 7 | applicationId "eu.vranckaert.hello.auto" 8 | minSdkVersion 26 9 | targetSdkVersion 26 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation fileTree(dir: 'libs', include: ['*.jar']) 24 | implementation 'com.android.support:appcompat-v7:26.1.0' 25 | implementation 'com.android.support:design:26.1.0' 26 | testImplementation 'junit:junit:4.12' 27 | androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.1', { 28 | exclude group: 'com.android.support', module: 'support-annotations' 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /app/libs/androidauto.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dirkvranckaert/AndroidAutoHelloWorld/80e34f4dc771fd749934523ddd03dd4fe7a73e12/app/libs/androidauto.jar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/eu/vranckaert/hello/auto/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package eu.vranckaert.hello.auto; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("eu.vranckaert.hello.auto", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/java/eu/vranckaert/hello/auto/HelloAndroidAutoActivity.java: -------------------------------------------------------------------------------- 1 | package eu.vranckaert.hello.auto; 2 | 3 | import android.os.Bundle; 4 | 5 | import com.google.android.apps.auto.sdk.CarActivity; 6 | 7 | /** 8 | * Created by dirkvranckaert on 09/10/2017. 9 | */ 10 | 11 | public class HelloAndroidAutoActivity extends CarActivity { 12 | @Override 13 | public void onCreate(Bundle bundle) { 14 | super.onCreate(bundle); 15 | setContentView(R.layout.activity_hello_aa); 16 | 17 | getCarUiController().getStatusBarController().setTitle("Hello AA"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/eu/vranckaert/hello/auto/HelloAndroidAutoActivityService.java: -------------------------------------------------------------------------------- 1 | package eu.vranckaert.hello.auto; 2 | 3 | import com.google.android.apps.auto.sdk.CarActivity; 4 | import com.google.android.apps.auto.sdk.CarActivityService; 5 | 6 | /** 7 | * Created by dirkvranckaert on 09/10/2017. 8 | */ 9 | 10 | public class HelloAndroidAutoActivityService extends CarActivityService { 11 | @Override 12 | public Class getCarActivity() { 13 | return HelloAndroidAutoActivity.class; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/eu/vranckaert/hello/auto/LoginActivity.java: -------------------------------------------------------------------------------- 1 | package eu.vranckaert.hello.auto; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.annotation.TargetApi; 6 | import android.content.pm.PackageManager; 7 | import android.support.annotation.NonNull; 8 | import android.support.design.widget.Snackbar; 9 | import android.support.v7.app.AppCompatActivity; 10 | import android.app.LoaderManager.LoaderCallbacks; 11 | 12 | import android.content.CursorLoader; 13 | import android.content.Loader; 14 | import android.database.Cursor; 15 | import android.net.Uri; 16 | import android.os.AsyncTask; 17 | 18 | import android.os.Build; 19 | import android.os.Bundle; 20 | import android.provider.ContactsContract; 21 | import android.text.TextUtils; 22 | import android.view.KeyEvent; 23 | import android.view.View; 24 | import android.view.View.OnClickListener; 25 | import android.view.inputmethod.EditorInfo; 26 | import android.widget.ArrayAdapter; 27 | import android.widget.AutoCompleteTextView; 28 | import android.widget.Button; 29 | import android.widget.EditText; 30 | import android.widget.TextView; 31 | 32 | import java.util.ArrayList; 33 | import java.util.List; 34 | 35 | import static android.Manifest.permission.READ_CONTACTS; 36 | 37 | /** 38 | * A login screen that offers login via email/password. 39 | */ 40 | public class LoginActivity extends AppCompatActivity implements LoaderCallbacks { 41 | 42 | /** 43 | * Id to identity READ_CONTACTS permission request. 44 | */ 45 | private static final int REQUEST_READ_CONTACTS = 0; 46 | 47 | /** 48 | * A dummy authentication store containing known user names and passwords. 49 | * TODO: remove after connecting to a real authentication system. 50 | */ 51 | private static final String[] DUMMY_CREDENTIALS = new String[]{ 52 | "foo@example.com:hello", "bar@example.com:world" 53 | }; 54 | /** 55 | * Keep track of the login task to ensure we can cancel it if requested. 56 | */ 57 | private UserLoginTask mAuthTask = null; 58 | 59 | // UI references. 60 | private AutoCompleteTextView mEmailView; 61 | private EditText mPasswordView; 62 | private View mProgressView; 63 | private View mLoginFormView; 64 | 65 | @Override 66 | protected void onCreate(Bundle savedInstanceState) { 67 | super.onCreate(savedInstanceState); 68 | setContentView(R.layout.activity_login); 69 | // Set up the login form. 70 | mEmailView = (AutoCompleteTextView) findViewById(R.id.email); 71 | populateAutoComplete(); 72 | 73 | mPasswordView = (EditText) findViewById(R.id.password); 74 | mPasswordView.setOnEditorActionListener(new TextView.OnEditorActionListener() { 75 | @Override 76 | public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) { 77 | if (id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL) { 78 | attemptLogin(); 79 | return true; 80 | } 81 | return false; 82 | } 83 | }); 84 | 85 | Button mEmailSignInButton = (Button) findViewById(R.id.email_sign_in_button); 86 | mEmailSignInButton.setOnClickListener(new OnClickListener() { 87 | @Override 88 | public void onClick(View view) { 89 | attemptLogin(); 90 | } 91 | }); 92 | 93 | mLoginFormView = findViewById(R.id.login_form); 94 | mProgressView = findViewById(R.id.login_progress); 95 | } 96 | 97 | private void populateAutoComplete() { 98 | if (!mayRequestContacts()) { 99 | return; 100 | } 101 | 102 | getLoaderManager().initLoader(0, null, this); 103 | } 104 | 105 | private boolean mayRequestContacts() { 106 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 107 | return true; 108 | } 109 | if (checkSelfPermission(READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) { 110 | return true; 111 | } 112 | if (shouldShowRequestPermissionRationale(READ_CONTACTS)) { 113 | Snackbar.make(mEmailView, R.string.permission_rationale, Snackbar.LENGTH_INDEFINITE) 114 | .setAction(android.R.string.ok, new View.OnClickListener() { 115 | @Override 116 | @TargetApi(Build.VERSION_CODES.M) 117 | public void onClick(View v) { 118 | requestPermissions(new String[]{READ_CONTACTS}, REQUEST_READ_CONTACTS); 119 | } 120 | }); 121 | } else { 122 | requestPermissions(new String[]{READ_CONTACTS}, REQUEST_READ_CONTACTS); 123 | } 124 | return false; 125 | } 126 | 127 | /** 128 | * Callback received when a permissions request has been completed. 129 | */ 130 | @Override 131 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, 132 | @NonNull int[] grantResults) { 133 | if (requestCode == REQUEST_READ_CONTACTS) { 134 | if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 135 | populateAutoComplete(); 136 | } 137 | } 138 | } 139 | 140 | 141 | /** 142 | * Attempts to sign in or register the account specified by the login form. 143 | * If there are form errors (invalid email, missing fields, etc.), the 144 | * errors are presented and no actual login attempt is made. 145 | */ 146 | private void attemptLogin() { 147 | if (mAuthTask != null) { 148 | return; 149 | } 150 | 151 | // Reset errors. 152 | mEmailView.setError(null); 153 | mPasswordView.setError(null); 154 | 155 | // Store values at the time of the login attempt. 156 | String email = mEmailView.getText().toString(); 157 | String password = mPasswordView.getText().toString(); 158 | 159 | boolean cancel = false; 160 | View focusView = null; 161 | 162 | // Check for a valid password, if the user entered one. 163 | if (!TextUtils.isEmpty(password) && !isPasswordValid(password)) { 164 | mPasswordView.setError(getString(R.string.error_invalid_password)); 165 | focusView = mPasswordView; 166 | cancel = true; 167 | } 168 | 169 | // Check for a valid email address. 170 | if (TextUtils.isEmpty(email)) { 171 | mEmailView.setError(getString(R.string.error_field_required)); 172 | focusView = mEmailView; 173 | cancel = true; 174 | } else if (!isEmailValid(email)) { 175 | mEmailView.setError(getString(R.string.error_invalid_email)); 176 | focusView = mEmailView; 177 | cancel = true; 178 | } 179 | 180 | if (cancel) { 181 | // There was an error; don't attempt login and focus the first 182 | // form field with an error. 183 | focusView.requestFocus(); 184 | } else { 185 | // Show a progress spinner, and kick off a background task to 186 | // perform the user login attempt. 187 | showProgress(true); 188 | mAuthTask = new UserLoginTask(email, password); 189 | mAuthTask.execute((Void) null); 190 | } 191 | } 192 | 193 | private boolean isEmailValid(String email) { 194 | //TODO: Replace this with your own logic 195 | return email.contains("@"); 196 | } 197 | 198 | private boolean isPasswordValid(String password) { 199 | //TODO: Replace this with your own logic 200 | return password.length() > 4; 201 | } 202 | 203 | /** 204 | * Shows the progress UI and hides the login form. 205 | */ 206 | @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2) 207 | private void showProgress(final boolean show) { 208 | // On Honeycomb MR2 we have the ViewPropertyAnimator APIs, which allow 209 | // for very easy animations. If available, use these APIs to fade-in 210 | // the progress spinner. 211 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) { 212 | int shortAnimTime = getResources().getInteger(android.R.integer.config_shortAnimTime); 213 | 214 | mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); 215 | mLoginFormView.animate().setDuration(shortAnimTime).alpha( 216 | show ? 0 : 1).setListener(new AnimatorListenerAdapter() { 217 | @Override 218 | public void onAnimationEnd(Animator animation) { 219 | mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); 220 | } 221 | }); 222 | 223 | mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); 224 | mProgressView.animate().setDuration(shortAnimTime).alpha( 225 | show ? 1 : 0).setListener(new AnimatorListenerAdapter() { 226 | @Override 227 | public void onAnimationEnd(Animator animation) { 228 | mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); 229 | } 230 | }); 231 | } else { 232 | // The ViewPropertyAnimator APIs are not available, so simply show 233 | // and hide the relevant UI components. 234 | mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); 235 | mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); 236 | } 237 | } 238 | 239 | @Override 240 | public Loader onCreateLoader(int i, Bundle bundle) { 241 | return new CursorLoader(this, 242 | // Retrieve data rows for the device user's 'profile' contact. 243 | Uri.withAppendedPath(ContactsContract.Profile.CONTENT_URI, 244 | ContactsContract.Contacts.Data.CONTENT_DIRECTORY), ProfileQuery.PROJECTION, 245 | 246 | // Select only email addresses. 247 | ContactsContract.Contacts.Data.MIMETYPE + 248 | " = ?", new String[]{ContactsContract.CommonDataKinds.Email 249 | .CONTENT_ITEM_TYPE}, 250 | 251 | // Show primary email addresses first. Note that there won't be 252 | // a primary email address if the user hasn't specified one. 253 | ContactsContract.Contacts.Data.IS_PRIMARY + " DESC"); 254 | } 255 | 256 | @Override 257 | public void onLoadFinished(Loader cursorLoader, Cursor cursor) { 258 | List emails = new ArrayList<>(); 259 | cursor.moveToFirst(); 260 | while (!cursor.isAfterLast()) { 261 | emails.add(cursor.getString(ProfileQuery.ADDRESS)); 262 | cursor.moveToNext(); 263 | } 264 | 265 | addEmailsToAutoComplete(emails); 266 | } 267 | 268 | @Override 269 | public void onLoaderReset(Loader cursorLoader) { 270 | 271 | } 272 | 273 | private void addEmailsToAutoComplete(List emailAddressCollection) { 274 | //Create adapter to tell the AutoCompleteTextView what to show in its dropdown list. 275 | ArrayAdapter adapter = 276 | new ArrayAdapter<>(LoginActivity.this, 277 | android.R.layout.simple_dropdown_item_1line, emailAddressCollection); 278 | 279 | mEmailView.setAdapter(adapter); 280 | } 281 | 282 | 283 | private interface ProfileQuery { 284 | String[] PROJECTION = { 285 | ContactsContract.CommonDataKinds.Email.ADDRESS, 286 | ContactsContract.CommonDataKinds.Email.IS_PRIMARY, 287 | }; 288 | 289 | int ADDRESS = 0; 290 | int IS_PRIMARY = 1; 291 | } 292 | 293 | /** 294 | * Represents an asynchronous login/registration task used to authenticate 295 | * the user. 296 | */ 297 | public class UserLoginTask extends AsyncTask { 298 | 299 | private final String mEmail; 300 | private final String mPassword; 301 | 302 | UserLoginTask(String email, String password) { 303 | mEmail = email; 304 | mPassword = password; 305 | } 306 | 307 | @Override 308 | protected Boolean doInBackground(Void... params) { 309 | // TODO: attempt authentication against a network service. 310 | 311 | try { 312 | // Simulate network access. 313 | Thread.sleep(2000); 314 | } catch (InterruptedException e) { 315 | return false; 316 | } 317 | 318 | for (String credential : DUMMY_CREDENTIALS) { 319 | String[] pieces = credential.split(":"); 320 | if (pieces[0].equals(mEmail)) { 321 | // Account exists, return true if the password matches. 322 | return pieces[1].equals(mPassword); 323 | } 324 | } 325 | 326 | // TODO: register the new account here. 327 | return true; 328 | } 329 | 330 | @Override 331 | protected void onPostExecute(final Boolean success) { 332 | mAuthTask = null; 333 | showProgress(false); 334 | 335 | if (success) { 336 | finish(); 337 | } else { 338 | mPasswordView.setError(getString(R.string.error_incorrect_password)); 339 | mPasswordView.requestFocus(); 340 | } 341 | } 342 | 343 | @Override 344 | protected void onCancelled() { 345 | mAuthTask = null; 346 | showProgress(false); 347 | } 348 | } 349 | } 350 | 351 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 17 | 22 | 27 | 32 | 37 | 42 | 47 | 52 | 57 | 62 | 67 | 72 | 77 | 82 | 87 | 92 | 97 | 102 | 107 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_hello_aa.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 21 | 22 | 26 | 27 | 32 | 33 | 36 | 37 | 45 | 46 | 47 | 48 | 51 | 52 | 63 | 64 | 65 | 66 |