[go: nahoru, domu]

Added User Interaction category to showcase app

Test: http://shortn/_4fEKZ3Qotv

Changes made:
User Interactions Demo Screen -
     a. Voice Interaction Demo
     b. Task Restriction Demo
     c. Request Permission Demos
          i. Request Permission
          ii. Pre-seed permissions

Change-Id: I0e54a8cd860978a01c80bddfa74f3801d58de854
diff --git a/car/app/app-samples/showcase/common/src/main/AndroidManifest.xml b/car/app/app-samples/showcase/common/src/main/AndroidManifest.xml
index 542568f..f44bb20 100644
--- a/car/app/app-samples/showcase/common/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/showcase/common/src/main/AndroidManifest.xml
@@ -17,9 +17,24 @@
 <manifest
     xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <!-- For accessing current location in PlaceListMapTemplate -->
-    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
-    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+    <permission-group android:name="android.permission-group.SHOWCASE"
+        android:label="@string/perm_group"
+        android:description="@string/perm_group_description" />
+
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
+        android:permissionGroup="android.permission-group.SHOWCASE"
+        android:label="@string/perm_fine_location"
+        android:description="@string/perm_fine_location_desc" />
+
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"
+        android:permissionGroup="android.permission-group.SHOWCASE"
+        android:label="@string/perm_coarse_location"
+        android:description="@string/perm_coarse_location_desc" />
+
+    <uses-permission android:name="android.permission.RECORD_AUDIO"
+        android:permissionGroup="android.permission-group.SHOWCASE"
+        android:label="@string/perm_record_audio"
+        android:description="@string/perm_record_audio_desc" />
 
     <application
         android:supportsRtl="true">
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/audio/VoiceInteraction.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/audio/VoiceInteraction.java
new file mode 100644
index 0000000..415fd73
--- /dev/null
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/audio/VoiceInteraction.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright 2022 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 androidx.car.app.sample.showcase.common.audio;
+
+import static android.Manifest.permission.RECORD_AUDIO;
+import static android.media.AudioAttributes.CONTENT_TYPE_MUSIC;
+import static android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE;
+import static android.media.AudioFormat.CHANNEL_OUT_MONO;
+import static android.media.AudioFormat.ENCODING_DEFAULT;
+import static android.media.AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+import static android.os.Build.VERSION.SDK_INT;
+
+import static androidx.car.app.media.CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE;
+import static androidx.car.app.media.CarAudioRecord.AUDIO_CONTENT_SAMPLING_RATE;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.media.AudioAttributes;
+import android.media.AudioFocusRequest;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioTrack;
+import android.os.Build.VERSION_CODES;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RequiresPermission;
+import androidx.car.app.CarContext;
+import androidx.car.app.CarToast;
+import androidx.car.app.media.CarAudioRecord;
+import androidx.car.app.utils.LogTags;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Manages recording the microphone and accessing the stored data from the microphone. */
+public class VoiceInteraction {
+    private static final String FILE_NAME = "recording.wav";
+
+    private final CarContext mCarContext;
+
+    public VoiceInteraction(@NonNull CarContext carContext) {
+        mCarContext = carContext;
+    }
+
+    /**
+     * Starts recording the car microphone, then plays it back.
+     */
+    @RequiresPermission(RECORD_AUDIO)
+    @SuppressLint("ClassVerificationFailure") // runtime check for < API 26
+    public void voiceInteractionDemo() {
+        // Some of the functions for recording require API level 26, so verify that first
+        if (SDK_INT < VERSION_CODES.O) {
+            CarToast.makeText(mCarContext, "API level is less than 26, "
+                            + "cannot use this functionality!",
+                    CarToast.LENGTH_LONG).show();
+            return;
+        }
+
+        // Check if we have permissions to record audio
+        if (!checkAudioPermission()) {
+            return;
+        }
+
+        // Start the thread for recording and playing back the audio
+        createRecordingThread().start();
+    }
+
+    /**
+     * Create thread which executes the record and the playback functions
+     */
+    @NonNull
+    @RequiresApi(api = VERSION_CODES.O)
+    @RequiresPermission(RECORD_AUDIO)
+    @SuppressLint("ClassVerificationFailure") // runtime check for < API 26
+    public Thread createRecordingThread() {
+        Thread recordingThread = new Thread(
+                () -> {
+                    // Request audio focus
+                    AudioFocusRequest audioFocusRequest = null;
+                    try {
+                        CarAudioRecord record = CarAudioRecord.create(mCarContext);
+                        // Take audio focus so that user's media is not recorded
+                        AudioAttributes audioAttributes =
+                                new AudioAttributes.Builder()
+                                        .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
+                                        .setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
+                                        .build();
+
+                        audioFocusRequest = new AudioFocusRequest.Builder(
+                                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
+                                .setAudioAttributes(audioAttributes)
+                                .setOnAudioFocusChangeListener(state -> {
+                                    if (state == AudioManager.AUDIOFOCUS_LOSS) {
+                                        // Stop recording if audio focus is lost
+                                        record.stopRecording();
+                                    }
+                                })
+                                .build();
+
+                        if (mCarContext.getSystemService(AudioManager.class)
+                                .requestAudioFocus(audioFocusRequest)
+                                != AUDIOFOCUS_REQUEST_GRANTED) {
+                            CarToast.makeText(mCarContext, "Audio Focus Request not granted",
+                                    CarToast.LENGTH_LONG).show();
+                            return;
+                        }
+                        recordAudio(record);
+                        playBackAudio();
+                    } catch (Exception e) {
+                        Log.e(LogTags.TAG, "Voice Interaction Error: ", e);
+                        throw new RuntimeException(e);
+                    } finally {
+                        // Abandon the FocusRequest so that user's media can be resumed
+                        mCarContext.getSystemService(AudioManager.class).abandonAudioFocusRequest(
+                                audioFocusRequest);
+                    }
+                },
+                "AudioRecorder Thread");
+        return recordingThread;
+    }
+
+    @RequiresPermission(RECORD_AUDIO)
+    private void playBackAudio() {
+
+        InputStream inputStream;
+        try {
+            inputStream = mCarContext.openFileInput(FILE_NAME);
+        } catch (FileNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+
+        AudioTrack audioTrack = new AudioTrack.Builder()
+                .setAudioAttributes(new AudioAttributes.Builder()
+                        .setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
+                        .setContentType(CONTENT_TYPE_MUSIC)
+                        .build())
+                .setAudioFormat(new AudioFormat.Builder()
+                        .setEncoding(ENCODING_DEFAULT)
+                        .setSampleRate(AUDIO_CONTENT_SAMPLING_RATE)
+                        .setChannelMask(CHANNEL_OUT_MONO)
+                        .build())
+                .setBufferSizeInBytes(AUDIO_CONTENT_BUFFER_SIZE)
+                .build();
+        audioTrack.play();
+        try {
+            byte[] audioData = new byte[AUDIO_CONTENT_BUFFER_SIZE];
+            while (inputStream.available() > 0) {
+                int readByteLength = inputStream.read(audioData, 0, audioData.length);
+
+                if (readByteLength < 0) {
+                    // End of file
+                    break;
+                }
+                audioTrack.write(audioData, 0, readByteLength);
+            }
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        audioTrack.stop();
+
+    }
+
+    @RequiresApi(api = VERSION_CODES.O)
+    @SuppressLint("ClassVerificationFailure") // runtime check for < API 26
+    @RequiresPermission(RECORD_AUDIO)
+    private void recordAudio(CarAudioRecord record) {
+
+        record.startRecording();
+
+        List<Byte> bytes = new ArrayList<>();
+        boolean isRecording = true;
+        while (isRecording) {
+            // gets the voice output from microphone to byte format
+            byte[] bData = new byte[AUDIO_CONTENT_BUFFER_SIZE];
+            int len = record.read(bData, 0, AUDIO_CONTENT_BUFFER_SIZE);
+
+            if (len > 0) {
+                for (int i = 0; i < len; i++) {
+                    bytes.add(bData[i]);
+                }
+            } else {
+                isRecording = false;
+            }
+        }
+
+        try {
+            OutputStream outputStream = mCarContext.openFileOutput(FILE_NAME, Context.MODE_PRIVATE);
+            addHeader(outputStream, bytes.size());
+            for (Byte b : bytes) {
+                outputStream.write(b);
+            }
+
+            outputStream.flush();
+            outputStream.close();
+        } catch (IOException e) {
+            throw new IllegalStateException(e);
+        }
+        record.stopRecording();
+    }
+
+    private static void addHeader(OutputStream outputStream, int totalAudioLen) throws IOException {
+        int totalDataLen = totalAudioLen + 36;
+        byte[] header = new byte[44];
+        int dataElementSize = 8;
+        long longSampleRate = AUDIO_CONTENT_SAMPLING_RATE;
+
+        // See http://soundfile.sapp.org/doc/WaveFormat/
+        header[0] = 'R';  // RIFF/WAVE header
+        header[1] = 'I';
+        header[2] = 'F';
+        header[3] = 'F';
+        header[4] = (byte) (totalAudioLen & 0xff);
+        header[5] = (byte) ((totalDataLen >> 8) & 0xff);
+        header[6] = (byte) ((totalDataLen >> 16) & 0xff);
+        header[7] = (byte) ((totalDataLen >> 24) & 0xff);
+        header[8] = 'W';
+        header[9] = 'A';
+        header[10] = 'V';
+        header[11] = 'E';
+        header[12] = 'f';  // 'fmt ' chunk
+        header[13] = 'm';
+        header[14] = 't';
+        header[15] = ' ';
+        header[16] = 16;  // 4 bytes: size of 'fmt ' chunk
+        header[17] = 0;
+        header[18] = 0;
+        header[19] = 0;
+        header[20] = 1;  // format = 1 PCM
+        header[21] = 0;
+        header[22] = 1; // Num channels (mono)
+        header[23] = 0;
+        header[24] = (byte) (longSampleRate & 0xff); // sample rate
+        header[25] = (byte) ((longSampleRate >> 8) & 0xff);
+        header[26] = (byte) ((longSampleRate >> 16) & 0xff);
+        header[27] = (byte) ((longSampleRate >> 24) & 0xff);
+        header[28] = (byte) (longSampleRate & 0xff); // byte rate
+        header[29] = (byte) ((longSampleRate >> 8) & 0xff);
+        header[30] = (byte) ((longSampleRate >> 16) & 0xff);
+        header[31] = (byte) ((longSampleRate >> 24) & 0xff);
+        header[32] = 1;  // block align
+        header[33] = 0;
+        header[34] = (byte) (dataElementSize & 0xff);  // bits per sample
+        header[35] = (byte) ((dataElementSize >> 8) & 0xff);
+        header[36] = 'd';
+        header[37] = 'a';
+        header[38] = 't';
+        header[39] = 'a';
+        header[40] = (byte) (totalAudioLen & 0xff);
+        header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
+        header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
+        header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
+
+        outputStream.write(header, 0, 44);
+    }
+
+    // Returns whether or not the user has granted audio permissions
+    private boolean checkAudioPermission() {
+        if (mCarContext.checkSelfPermission(RECORD_AUDIO)
+                != PackageManager.PERMISSION_GRANTED) {
+            CarToast.makeText(mCarContext, "Grant mic permission on device",
+                    CarToast.LENGTH_LONG).show();
+            List<String> permissions = Collections.singletonList(RECORD_AUDIO);
+            mCarContext.requestPermissions(permissions, (grantedPermissions,
+                    rejectedPermissions) -> {
+                if (grantedPermissions.contains(RECORD_AUDIO)) {
+                    voiceInteractionDemo();
+                }
+            });
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/UserInteractionsDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/UserInteractionsDemoScreen.java
new file mode 100644
index 0000000..8bd55d5
--- /dev/null
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/UserInteractionsDemoScreen.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2022 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 androidx.car.app.sample.showcase.common.screens;
+
+import static androidx.car.app.model.Action.BACK;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.CarContext;
+import androidx.car.app.Screen;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.Item;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.ListTemplate;
+import androidx.car.app.model.MessageTemplate;
+import androidx.car.app.model.OnClickListener;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Template;
+import androidx.car.app.sample.showcase.common.R;
+import androidx.car.app.sample.showcase.common.audio.VoiceInteraction;
+import androidx.car.app.sample.showcase.common.screens.userinteractions.RequestPermissionMenuDemoScreen;
+import androidx.core.graphics.drawable.IconCompat;
+
+/** A screen demonstrating User Interactions */
+public final class UserInteractionsDemoScreen extends Screen {
+    private static final int MAX_STEPS_ALLOWED = 4;
+
+    private final int mStep;
+    private boolean mIsBackOperation;
+
+    public UserInteractionsDemoScreen(int step, @NonNull CarContext carContext) {
+        super(carContext);
+        this.mStep = step;
+    }
+
+    @NonNull
+    @Override
+    public Template onGetTemplate() {
+
+        // Last step must either be a PaneTemplate, MessageTemplate or NavigationTemplate.
+        if (mStep == MAX_STEPS_ALLOWED) {
+            return templateForTaskLimitReached();
+        }
+
+        ItemList.Builder builder = new ItemList.Builder();
+
+        builder.addItem(buildRowForVoiceInteractionDemo());
+        builder.addItem(buildRowForRequestPermissionsDemo());
+        builder.addItem(buildRowForTaskRestrictionDemo());
+
+        if (mIsBackOperation) {
+            builder.addItem(buildRowForAdditionalData());
+        }
+
+        return new ListTemplate.Builder()
+                .setSingleList(builder.build())
+                .setTitle(getCarContext().getString(R.string.user_interactions_demo_title))
+                .setHeaderAction(BACK)
+                .setActionStrip(
+                        new ActionStrip.Builder()
+                                .addAction(
+                                        new Action.Builder()
+                                                .setTitle(getCarContext().getString(
+                                                        R.string.home_caps_action_title))
+                                                .setOnClickListener(
+                                                        () -> getScreenManager().popToRoot())
+                                                .build())
+                                .build())
+                .build();
+
+    }
+
+    /**
+     * Returns the row for VoiceInteraction Demo
+     */
+    private Item buildRowForVoiceInteractionDemo() {
+        return new Row.Builder()
+                .setTitle(getCarContext().getString(R.string.voice_access_demo_title))
+                .setImage(new CarIcon.Builder(
+                        IconCompat.createWithResource(
+                                getCarContext(),
+                                R.drawable.ic_mic))
+                        .build(), Row.IMAGE_TYPE_ICON)
+                .setOnClickListener(new VoiceInteraction(getCarContext())::voiceInteractionDemo)
+                .build();
+    }
+
+    /**
+     * Returns the row for TaskRestriction Demo
+     */
+    private Item buildRowForTaskRestrictionDemo() {
+        return new Row.Builder()
+                .setTitle(getCarContext().getString(R.string.task_step_of_title, mStep,
+                        MAX_STEPS_ALLOWED))
+                .addText(getCarContext().getString(R.string.task_step_of_text))
+                .setImage(new CarIcon.Builder(
+                        IconCompat.createWithResource(
+                                getCarContext(), R.drawable.baseline_task_24))
+                        .build(), Row.IMAGE_TYPE_ICON)
+                .setOnClickListener(
+                        () ->
+                                getScreenManager()
+                                        .pushForResult(
+                                                new UserInteractionsDemoScreen(
+                                                        mStep + 1, getCarContext()),
+                                                result -> mIsBackOperation = true))
+                .build();
+    }
+
+    /**
+     * Returns the row for RequestPermissions Demo
+     */
+    private Item buildRowForRequestPermissionsDemo() {
+        return new Row.Builder()
+                .setTitle(getCarContext().getString(
+                        R.string.request_permission_menu_demo_title))
+                .setImage(new CarIcon.Builder(
+                        IconCompat.createWithResource(
+                                getCarContext(),
+                                R.drawable.baseline_question_mark_24))
+                        .build(), Row.IMAGE_TYPE_ICON)
+                .setOnClickListener(() -> getScreenManager().push(
+                        new RequestPermissionMenuDemoScreen(getCarContext())))
+                .build();
+    }
+
+    /**
+     * Returns the row for AdditionalData
+     */
+    private Item buildRowForAdditionalData() {
+        return new Row.Builder()
+                .setTitle(getCarContext().getString(R.string.additional_data_title))
+                .addText(getCarContext().getString(R.string.additional_data_text))
+                .build();
+    }
+
+    /**
+     * Returns the MessageTemplate with "Task Limit Reached" message after user completes 4 task
+     * steps
+     */
+    private MessageTemplate templateForTaskLimitReached() {
+        OnClickListener  ->
+                getScreenManager()
+                        .pushForResult(
+                                new UserInteractionsDemoScreen(
+                                        mStep + 1,
+                                        getCarContext()),
+                                result ->
+                                        mIsBackOperation = true);
+
+        return new MessageTemplate.Builder(
+                getCarContext().getString(R.string.task_limit_reached_msg))
+                .setHeaderAction(BACK)
+                .addAction(
+                        new Action.Builder()
+                                .setTitle(getCarContext().getString(
+                                        R.string.try_anyway_action_title))
+                                .setOnClickListener(onClickListener)
+                                .build())
+                .build();
+    }
+
+}
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/PreSeedPermissionScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/PreSeedPermissionScreen.java
new file mode 100644
index 0000000..f82949d
--- /dev/null
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/PreSeedPermissionScreen.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2022 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 androidx.car.app.sample.showcase.common.screens.userinteractions;
+
+import static androidx.car.app.model.Action.BACK;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.CarContext;
+import androidx.car.app.Screen;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.MessageTemplate;
+import androidx.car.app.model.Template;
+import androidx.car.app.sample.showcase.common.R;
+import androidx.car.app.sample.showcase.common.ShowcaseService;
+
+/** A screen that demonstrates exiting the app and pre-seeding it with a request for permissions */
+public class PreSeedPermissionScreen extends Screen {
+    public PreSeedPermissionScreen(@NonNull CarContext carContext) {
+        super(carContext);
+    }
+
+    @NonNull
+    @Override
+    public Template onGetTemplate() {
+        return new MessageTemplate.Builder(getCarContext().getString(R.string.finish_app_msg))
+                .setTitle(getCarContext().getString(R.string.preseed_permission_app_title))
+                .setHeaderAction(BACK)
+                .addAction(
+                        new Action.Builder()
+                                .setOnClickListener(
+                                        () -> {
+                                            getCarContext()
+                                                    .getSharedPreferences(
+                                                            ShowcaseService.SHARED_PREF_KEY,
+                                                            Context.MODE_PRIVATE)
+                                                    .edit()
+                                                    .putBoolean(
+                                                            ShowcaseService.PRE_SEED_KEY, true)
+                                                    .apply();
+                                            getCarContext().finishCarApp();
+                                        })
+                                .setTitle(getCarContext().getString(R.string.exit_action_title))
+                                .build())
+                .build();
+    }
+}
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/RequestPermissionMenuDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/RequestPermissionMenuDemoScreen.java
new file mode 100644
index 0000000..875d1df
--- /dev/null
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/RequestPermissionMenuDemoScreen.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2022 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 androidx.car.app.sample.showcase.common.screens.userinteractions;
+
+import static androidx.car.app.model.Action.BACK;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.CarContext;
+import androidx.car.app.Screen;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.ListTemplate;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Template;
+import androidx.car.app.sample.showcase.common.R;
+import androidx.lifecycle.DefaultLifecycleObserver;
+
+/** Screen to list different permission demos */
+public final class RequestPermissionMenuDemoScreen extends Screen
+        implements DefaultLifecycleObserver {
+
+    public RequestPermissionMenuDemoScreen(@NonNull CarContext carContext) {
+        super(carContext);
+        getLifecycle().addObserver(this);
+    }
+
+    @NonNull
+    @Override
+    public Template onGetTemplate() {
+        ItemList.Builder listBuilder = new ItemList.Builder();
+
+        listBuilder.addItem(
+                new Row.Builder()
+                        .setTitle(getCarContext().getString(R.string.request_permissions_title))
+                        .setOnClickListener(() ->
+                                getScreenManager().push(
+                                        new RequestPermissionScreen(getCarContext())))
+                        .setBrowsable(true)
+                        .build());
+        listBuilder.addItem(
+                new Row.Builder()
+                        .setTitle(getCarContext().getString(R.string.preseed_permission_demo_title))
+                        .setOnClickListener(() ->
+                                getScreenManager().push(
+                                        new PreSeedPermissionScreen(getCarContext())))
+                        .setBrowsable(true)
+                        .build());
+        return new ListTemplate.Builder()
+                .setSingleList(listBuilder.build())
+                .setTitle(getCarContext().getString(R.string.request_permission_menu_demo_title))
+                .setHeaderAction(BACK)
+                .build();
+    }
+}
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/RequestPermissionScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/RequestPermissionScreen.java
new file mode 100644
index 0000000..91f6583
--- /dev/null
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/RequestPermissionScreen.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2022 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 androidx.car.app.sample.showcase.common.screens.userinteractions;
+
+import static android.content.pm.PackageManager.FEATURE_AUTOMOTIVE;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.location.LocationManager;
+import android.provider.Settings;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.CarAppPermission;
+import androidx.car.app.CarContext;
+import androidx.car.app.CarToast;
+import androidx.car.app.Screen;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.LongMessageTemplate;
+import androidx.car.app.model.MessageTemplate;
+import androidx.car.app.model.OnClickListener;
+import androidx.car.app.model.ParkedOnlyOnClickListener;
+import androidx.car.app.model.Template;
+import androidx.car.app.sample.showcase.common.R;
+import androidx.core.location.LocationManagerCompat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A screen to show a request for a runtime permission from the user.
+ *
+ * <p>Scans through the possible dangerous permissions and shows which ones have not been
+ * granted in the message. Clicking on the action button will launch the permission request on
+ * the phone.
+ *
+ * <p>If all permissions are granted, corresponding message is displayed with a refresh button which
+ * will scan again when clicked.
+ */
+public class RequestPermissionScreen extends Screen {
+    /**
+     * This field can and should be removed once b/192386096 and/or b/192385602 have been resolved.
+     * Currently it is not possible to know the level of the screen stack and determine the
+     * header action according to that. A boolean flag is added to determine that temporarily.
+     */
+    private final boolean mPreSeedMode;
+    private final String mCarAppPermissionsPrefix = "androidx.car.app";
+
+    /**
+     * Action which invalidates the template.
+     *
+     * <p>This can give the user a chance to revoke the permissions and then refresh will pickup
+     * the permissions that need to be granted.
+     */
+    private final Action mRefreshAction = new Action.Builder()
+            .setTitle(getCarContext().getString(R.string.refresh_action_title))
+            .setBackgroundColor(CarColor.BLUE)
+            .setOnClickListener(this::invalidate)
+            .build();
+
+    public RequestPermissionScreen(@NonNull CarContext carContext) {
+        this(carContext, false);
+    }
+
+    public RequestPermissionScreen(@NonNull CarContext carContext, boolean preSeedMode) {
+        super(carContext);
+        this.mPreSeedMode = preSeedMode;
+    }
+
+    @NonNull
+    @Override
+    @SuppressWarnings("deprecation")
+    public Template onGetTemplate() {
+        Action headerAction = mPreSeedMode ? Action.APP_ICON : Action.BACK;
+        List<String> permissions = new ArrayList<>();
+        String[] declaredPermissions;
+        try {
+            PackageInfo info =
+                    getCarContext().getPackageManager().getPackageInfo(
+                            getCarContext().getPackageName(),
+                            PackageManager.GET_PERMISSIONS);
+            declaredPermissions = info.requestedPermissions;
+        } catch (PackageManager.NameNotFoundException e) {
+            return new MessageTemplate.Builder(
+                    getCarContext().getString(R.string.package_not_found_error_msg))
+                    .setHeaderAction(headerAction)
+                    .addAction(mRefreshAction)
+                    .build();
+        }
+
+        if (declaredPermissions != null) {
+            for (String declaredPermission : declaredPermissions) {
+                // Don't include permissions against the car app host as they are all normal but
+                // show up as ungranted by the system.
+                if (declaredPermission.startsWith(mCarAppPermissionsPrefix)) {
+                    continue;
+                }
+                try {
+                    CarAppPermission.checkHasPermission(getCarContext(), declaredPermission);
+                } catch (SecurityException e) {
+                    permissions.add(declaredPermission);
+                }
+            }
+        }
+        if (permissions.isEmpty()) {
+            return new MessageTemplate.Builder(
+                    getCarContext().getString(R.string.permissions_granted_msg))
+                    .setHeaderAction(headerAction)
+                    .addAction(new Action.Builder()
+                            .setTitle(getCarContext().getString(R.string.close_action_title))
+                            .setOnClickListener(this::finish)
+                            .build())
+                    .build();
+        }
+
+        StringBuilder message = new StringBuilder()
+                .append(getCarContext().getString(R.string.needs_access_msg_prefix));
+        for (String permission : permissions) {
+            message.append(permission);
+            message.append("\n");
+        }
+
+        OnClickListener listener = ParkedOnlyOnClickListener.create(() -> {
+            getCarContext().requestPermissions(
+                    permissions,
+                    (approved, rejected) -> {
+                        CarToast.makeText(
+                                getCarContext(),
+                                String.format("Approved: %s Rejected: %s", approved, rejected),
+                                CarToast.LENGTH_LONG).show();
+                    });
+            if (!getCarContext().getPackageManager().hasSystemFeature(FEATURE_AUTOMOTIVE)) {
+                CarToast.makeText(getCarContext(),
+                        getCarContext().getString(R.string.phone_screen_permission_msg),
+                        CarToast.LENGTH_LONG).show();
+            }
+        });
+
+        Action action = new Action.Builder()
+                .setTitle(getCarContext().getString(R.string.grant_access_action_title))
+                .setBackgroundColor(CarColor.BLUE)
+                .setOnClickListener(listener)
+                .build();
+
+
+        Action action2 = null;
+        LocationManager locationManager =
+                (LocationManager) getCarContext().getSystemService(Context.LOCATION_SERVICE);
+        if (!LocationManagerCompat.isLocationEnabled(locationManager)) {
+            message.append(
+                    getCarContext().getString(R.string.enable_location_permission_on_device_msg));
+            message.append("\n");
+            action2 = new Action.Builder()
+                    .setTitle(getCarContext().getString(R.string.enable_location_action_title))
+                    .setBackgroundColor(CarColor.BLUE)
+                    .setOnClickListener(ParkedOnlyOnClickListener.create(() -> {
+                        getCarContext().startActivity(
+                                new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).addFlags(
+                                        Intent.FLAG_ACTIVITY_NEW_TASK));
+                        if (!getCarContext().getPackageManager().hasSystemFeature(
+                                FEATURE_AUTOMOTIVE)) {
+                            CarToast.makeText(getCarContext(),
+                                    getCarContext().getString(
+                                            R.string.enable_location_permission_on_phone_msg),
+                                    CarToast.LENGTH_LONG).show();
+                        }
+                    }))
+                    .build();
+        }
+
+
+        LongMessageTemplate.Builder builder = new LongMessageTemplate.Builder(message)
+                .setTitle(getCarContext().getString(R.string.required_permissions_title))
+                .addAction(action)
+                .setHeaderAction(headerAction);
+
+        if (action2 != null) {
+            builder.addAction(action2);
+        }
+
+        return builder.build();
+    }
+}
diff --git a/car/app/app-samples/showcase/common/src/main/res/drawable-hdpi/ic_mic.xml b/car/app/app-samples/showcase/common/src/main/res/drawable-hdpi/ic_mic.xml
new file mode 100644
index 0000000..57d2b5191
--- /dev/null
+++ b/car/app/app-samples/showcase/common/src/main/res/drawable-hdpi/ic_mic.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2022 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="40dp"
+    android:height="40dp"
+    android:viewportWidth="40"
+    android:viewportHeight="40">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M20,22.708Q18.125,22.708 16.833,21.354Q15.542,20 15.542,18.083V7.792Q15.542,5.917 16.833,4.625Q18.125,3.333 20,3.333Q21.875,3.333 23.167,4.625Q24.458,5.917 24.458,7.792V18.083Q24.458,20 23.167,21.354Q21.875,22.708 20,22.708ZM20,13.042Q20,13.042 20,13.042Q20,13.042 20,13.042Q20,13.042 20,13.042Q20,13.042 20,13.042Q20,13.042 20,13.042Q20,13.042 20,13.042Q20,13.042 20,13.042Q20,13.042 20,13.042ZM18.625,35V29.5Q14.208,29 11.271,25.75Q8.333,22.5 8.333,18.083H11.125Q11.125,21.75 13.729,24.292Q16.333,26.833 20,26.833Q23.667,26.833 26.271,24.292Q28.875,21.75 28.875,18.083H31.667Q31.667,22.5 28.729,25.75Q25.792,29 21.375,29.5V35ZM20,19.917Q20.75,19.917 21.229,19.375Q21.708,18.833 21.708,18.083V7.792Q21.708,7.083 21.208,6.604Q20.708,6.125 20,6.125Q19.292,6.125 18.792,6.604Q18.292,7.083 18.292,7.792V18.083Q18.292,18.833 18.771,19.375Q19.25,19.917 20,19.917Z"/>
+</vector>
\ No newline at end of file
diff --git a/car/app/app-samples/showcase/common/src/main/res/drawable/baseline_question_mark_24.xml b/car/app/app-samples/showcase/common/src/main/res/drawable/baseline_question_mark_24.xml
new file mode 100644
index 0000000..fbfe98d
--- /dev/null
+++ b/car/app/app-samples/showcase/common/src/main/res/drawable/baseline_question_mark_24.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M11.07,12.85c0.77,-1.39 2.25,-2.21 3.11,-3.44c0.91,-1.29 0.4,-3.7 -2.18,-3.7c-1.69,0 -2.52,1.28 -2.87,2.34L6.54,6.96C7.25,4.83 9.18,3 11.99,3c2.35,0 3.96,1.07 4.78,2.41c0.7,1.15 1.11,3.3 0.03,4.9c-1.2,1.77 -2.35,2.31 -2.97,3.45c-0.25,0.46 -0.35,0.76 -0.35,2.24h-2.89C10.58,15.22 10.46,13.95 11.07,12.85zM14,20c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2S14,18.9 14,20z"/>
+</vector>
diff --git a/car/app/app-samples/showcase/common/src/main/res/drawable/baseline_task_24.xml b/car/app/app-samples/showcase/common/src/main/res/drawable/baseline_task_24.xml
new file mode 100644
index 0000000..fce940d
--- /dev/null
+++ b/car/app/app-samples/showcase/common/src/main/res/drawable/baseline_task_24.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M14,2H6C4.9,2 4.01,2.9 4.01,4L4,20c0,1.1 0.89,2 1.99,2H18c1.1,0 2,-0.9 2,-2V8L14,2zM10.94,18L7.4,14.46l1.41,-1.41l2.12,2.12l4.24,-4.24l1.41,1.41L10.94,18zM13,9V3.5L18.5,9H13z"/>
+</vector>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values/strings.xml
index c4d6f70..45f07ef 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values/strings.xml
@@ -130,6 +130,8 @@
   <string name="finish_app_msg">This will finish the app, and when you return it will pre-seed a permission screen</string>
   <string name="finish_app_title">Finish App Demo</string>
   <string name="finish_app_demo_title">Pre-seed the Permission Screen on next run Demo</string>
+  <string name="preseed_permission_app_title">Pre-seed permission App Demo</string>
+  <string name="preseed_permission_demo_title">Pre-seed the Permission Screen on next run Demo</string>
 
   <!-- LoadingDemoScreen -->
   <string name="loading_demo_title">Loading Demo</string>
@@ -387,6 +389,21 @@
   <string name="cal_api_level_prefix" translatable="false">CAL API Level: %d</string>
   <string name="showcase_demos_title">Showcase Demos</string>
 
+  <!-- User Interactions Screen -->
+  <string name="voice_access_demo_title">Voice Access Demo Screen</string>
+  <string name="user_interactions_demo_title">User Interactions</string>
+  <string name="request_permission_menu_demo_title">Request Permissions Demos</string>
+
+  <!-- Manifest file permissions -->
+  <string name="perm_group">Permission Group</string>
+  <string name="perm_group_description">Permission Group for Showcase App</string>
+  <string name="perm_fine_location">Access to Fine Location</string>
+  <string name="perm_fine_location_desc">Permission for Access to Fine Location</string>
+  <string name="perm_coarse_location">Access to Coarse Location</string>
+  <string name="perm_coarse_location_desc">Permission for Access to Coarse Location</string>
+  <string name="perm_record_audio">Access to Record Audio</string>
+  <string name="perm_record_audio_desc">Permission for Access to Record Audio</string>
+
   <!-- Location Strings -->
   <string name="location_1_title" translatable="false">Google Kirkland</string>
   <string name="location_1_address" translatable="false">747 6th St South, Kirkland, WA 98033</string>