From f75e01a8af96802d885d761f3c83aeffb67001fb Mon Sep 17 00:00:00 2001 From: Tiem Song Date: Tue, 5 Sep 2017 11:44:54 -0700 Subject: [PATCH 01/85] Update Camera2Basic sample based on feedback during Android App Week. - Replace android.app.Fragment with v4 support Fragment + updated related activity / theme - Call abortCaptures() to fix bug with multiple image captures after pressing the shutter button - Update copyright dates - Minor Android Studio warning fixes Test: Manually ran app before/after changes and verified there were no regressions. Change-Id: I735cba5f0d3de091ba86f7fc8d8caf6f8338ebb0 --- .../camera2basic/Camera2BasicFragment.java | 24 ++++++++++--------- .../android/camera2basic/CameraActivity.java | 8 +++---- .../src/main/res/values/styles.xml | 6 +++-- media/Camera2Basic/README.md | 6 ++--- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/media/Camera2Basic/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java b/media/Camera2Basic/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java index a29fa144..aca77f7f 100644 --- a/media/Camera2Basic/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java +++ b/media/Camera2Basic/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 The Android Open Source Project + * Copyright 2017 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. @@ -20,8 +20,6 @@ import android.Manifest; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; -import android.app.DialogFragment; -import android.app.Fragment; import android.content.Context; import android.content.DialogInterface; import android.content.pm.PackageManager; @@ -47,7 +45,9 @@ import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.support.annotation.NonNull; -import android.support.v13.app.FragmentCompat; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; import android.util.Log; import android.util.Size; @@ -72,7 +72,7 @@ import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; public class Camera2BasicFragment extends Fragment - implements View.OnClickListener, FragmentCompat.OnRequestPermissionsResultCallback { + implements View.OnClickListener, ActivityCompat.OnRequestPermissionsResultCallback { /** * Conversion from screen rotation to JPEG orientation. @@ -461,11 +461,10 @@ public class Camera2BasicFragment extends Fragment } private void requestCameraPermission() { - if (FragmentCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) { + if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) { new ConfirmationDialog().show(getChildFragmentManager(), FRAGMENT_DIALOG); } else { - FragmentCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, - REQUEST_CAMERA_PERMISSION); + requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION); } } @@ -488,6 +487,7 @@ public class Camera2BasicFragment extends Fragment * @param width The width of available size for camera preview * @param height The height of available size for camera preview */ + @SuppressWarnings("SuspiciousNameCombination") private void setUpCameraOutputs(int width, int height) { Activity activity = getActivity(); CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); @@ -842,6 +842,7 @@ public class Camera2BasicFragment extends Fragment }; mCaptureSession.stopRepeating(); + mCaptureSession.abortCaptures(); mCaptureSession.capture(captureBuilder.build(), CaptureCallback, null); } catch (CameraAccessException e) { e.printStackTrace(); @@ -924,7 +925,7 @@ public class Camera2BasicFragment extends Fragment */ private final File mFile; - public ImageSaver(Image image, File file) { + ImageSaver(Image image, File file) { mImage = image; mFile = file; } @@ -983,6 +984,7 @@ public class Camera2BasicFragment extends Fragment return dialog; } + @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final Activity activity = getActivity(); @@ -1004,6 +1006,7 @@ public class Camera2BasicFragment extends Fragment */ public static class ConfirmationDialog extends DialogFragment { + @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final Fragment parent = getParentFragment(); @@ -1012,8 +1015,7 @@ public class Camera2BasicFragment extends Fragment .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - FragmentCompat.requestPermissions(parent, - new String[]{Manifest.permission.CAMERA}, + parent.requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION); } }) diff --git a/media/Camera2Basic/Application/src/main/java/com/example/android/camera2basic/CameraActivity.java b/media/Camera2Basic/Application/src/main/java/com/example/android/camera2basic/CameraActivity.java index eb7f8343..fab28dd8 100644 --- a/media/Camera2Basic/Application/src/main/java/com/example/android/camera2basic/CameraActivity.java +++ b/media/Camera2Basic/Application/src/main/java/com/example/android/camera2basic/CameraActivity.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 The Android Open Source Project + * Copyright 2017 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. @@ -16,17 +16,17 @@ package com.example.android.camera2basic; -import android.app.Activity; import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; -public class CameraActivity extends Activity { +public class CameraActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_camera); if (null == savedInstanceState) { - getFragmentManager().beginTransaction() + getSupportFragmentManager().beginTransaction() .replace(R.id.container, Camera2BasicFragment.newInstance()) .commit(); } diff --git a/media/Camera2Basic/Application/src/main/res/values/styles.xml b/media/Camera2Basic/Application/src/main/res/values/styles.xml index 3f3bdfb4..b76b27db 100644 --- a/media/Camera2Basic/Application/src/main/res/values/styles.xml +++ b/media/Camera2Basic/Application/src/main/res/values/styles.xml @@ -1,5 +1,5 @@ - diff --git a/media/Camera2Basic/README.md b/media/Camera2Basic/README.md index cb8ca9f1..2bf96eaf 100644 --- a/media/Camera2Basic/README.md +++ b/media/Camera2Basic/README.md @@ -42,8 +42,8 @@ when you are done. Pre-requisites -------------- -- Android SDK 24 -- Android Build Tools v24.0.2 +- Android SDK 25 +- Android Build Tools v25.0.3 - Android Support Repository Screenshots @@ -72,7 +72,7 @@ submitting a pull request through GitHub. Please see CONTRIBUTING.md for more de License ------- -Copyright 2016 The Android Open Source Project, Inc. +Copyright 2017 The Android Open Source Project, Inc. Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for -- GitLab From 2f0f033a3dcec3c3ee71b7060126298ddc625534 Mon Sep 17 00:00:00 2001 From: Tiem Song Date: Mon, 18 Sep 2017 10:45:25 -0700 Subject: [PATCH 02/85] Update Notifications sample - prep work for Kotlin version Based on the feedback in ag/2799773 from benweiss@. Bug: 65631986 Test: Manually ran app on emulated phone and watch, verified that there are no regressions Change-Id: I4b2bd955e6eb3325b76e7b36669bbab6eaba68f6 --- .../wearable/notifications/MainActivity.java | 76 +++--- .../main/res/drawable/selected_background.xml | 12 +- .../res/drawable/unselected_background.xml | 12 +- .../src/main/res/layout/activity_main.xml | 234 ++++++++++++++++++ .../Application/src/main/res/layout/main.xml | 173 ------------- .../src/main/res/values/colors.xml | 3 +- .../src/main/res/values/dimens.xml | 2 + .../layout/activity_notification_display.xml | 4 +- .../Wearable/src/main/res/values/colors.xml | 2 + 9 files changed, 298 insertions(+), 220 deletions(-) create mode 100644 wearable/wear/Notifications/Application/src/main/res/layout/activity_main.xml delete mode 100644 wearable/wear/Notifications/Application/src/main/res/layout/main.xml diff --git a/wearable/wear/Notifications/Application/src/main/java/com/example/android/support/wearable/notifications/MainActivity.java b/wearable/wear/Notifications/Application/src/main/java/com/example/android/support/wearable/notifications/MainActivity.java index 4ade3ed1..e0b7c980 100644 --- a/wearable/wear/Notifications/Application/src/main/java/com/example/android/support/wearable/notifications/MainActivity.java +++ b/wearable/wear/Notifications/Application/src/main/java/com/example/android/support/wearable/notifications/MainActivity.java @@ -58,12 +58,12 @@ public class MainActivity extends Activity implements Handler.Callback { private CheckBox mIncludeContentIntentCheckbox; private CheckBox mVibrateCheckbox; private BackgroundPickers mBackgroundPickers; - private int postedNotificationCount = 0; + private int mPostedNotificationCount = 0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.main); + setContentView(R.layout.activity_main); mHandler = new Handler(this); mTextChangedListener = new UpdateNotificationsOnTextChangeListener(); @@ -172,15 +172,25 @@ public class MainActivity extends Activity implements Handler.Callback { private void updateTextEditors(NotificationPreset preset) { mTitleEditText.setText(getString(preset.titleResId)); mTextEditText.setText(getString(preset.textResId)); + + View[] presetViews = { + findViewById(R.id.title_label), + findViewById(R.id.title_editor), + findViewById(R.id.text_label), + findViewById(R.id.text_editor) + }; + if (preset == NotificationPresets.BASIC) { - findViewById(R.id.title_edit_field).setVisibility(View.VISIBLE); + for (View v : presetViews) { + v.setVisibility(View.VISIBLE); + } mTitleEditText.addTextChangedListener(mTextChangedListener); - findViewById(R.id.text_edit_field).setVisibility(View.VISIBLE); mTextEditText.addTextChangedListener(mTextChangedListener); } else { - findViewById(R.id.title_edit_field).setVisibility(View.GONE); + for (View v : presetViews) { + v.setVisibility(View.GONE); + } mTitleEditText.removeTextChangedListener(mTextChangedListener); - findViewById(R.id.text_edit_field).setVisibility(View.GONE); mTextEditText.removeTextChangedListener(mTextChangedListener); } } @@ -195,9 +205,10 @@ public class MainActivity extends Activity implements Handler.Callback { if (cancelExisting) { // Cancel all existing notifications to trigger fresh-posting behavior: For example, - // switching from HIGH to LOW priority does not cause a reordering in Notification Shade. + // switching from HIGH to LOW priority does not cause a reordering in Notification + // Shade. NotificationManagerCompat.from(this).cancelAll(); - postedNotificationCount = 0; + mPostedNotificationCount = 0; // Post the updated notifications on a delay to avoid a cancel+post race condition // with notification manager. @@ -215,12 +226,26 @@ public class MainActivity extends Activity implements Handler.Callback { sendBroadcast(new Intent(NotificationIntentReceiver.ACTION_ENABLE_MESSAGES) .setClass(this, NotificationIntentReceiver.class)); + Notification[] notifications = buildNotifications(); + + // Post new notifications + for (int i = 0; i < notifications.length; i++) { + NotificationManagerCompat.from(this).notify(i, notifications[i]); + } + // Cancel any that are beyond the current count. + for (int i = notifications.length; i < mPostedNotificationCount; i++) { + NotificationManagerCompat.from(this).cancel(i); + } + mPostedNotificationCount = notifications.length; + } + + /** + * Build the sample notifications + */ + + private Notification[] buildNotifications() { NotificationPreset preset = NotificationPresets.PRESETS[ mPresetSpinner.getSelectedItemPosition()]; - CharSequence titlePreset = mTitleEditText.getText(); - CharSequence textPreset = mTextEditText.getText(); - PriorityPreset priorityPreset = PriorityPresets.PRESETS[ - mPrioritySpinner.getSelectedItemPosition()]; ActionsPreset actionsPreset = ActionsPresets.PRESETS[ mActionsSpinner.getSelectedItemPosition()]; if (preset.actionsRequired() && actionsPreset == ActionsPresets.NO_ACTIONS_PRESET) { @@ -230,26 +255,16 @@ public class MainActivity extends Activity implements Handler.Callback { actionsPreset), true); } NotificationPreset.BuildOptions options = new NotificationPreset.BuildOptions( - titlePreset, - textPreset, - priorityPreset, + mTitleEditText.getText(), + mTextEditText.getText(), + PriorityPresets.PRESETS[mPrioritySpinner.getSelectedItemPosition()], actionsPreset, mIncludeLargeIconCheckbox.isChecked(), mLocalOnlyCheckbox.isChecked(), mIncludeContentIntentCheckbox.isChecked(), mVibrateCheckbox.isChecked(), mBackgroundPickers.getRes()); - Notification[] notifications = preset.buildNotifications(this, options); - - // Post new notifications - for (int i = 0; i < notifications.length; i++) { - NotificationManagerCompat.from(this).notify(i, notifications[i]); - } - // Cancel any that are beyond the current count. - for (int i = notifications.length; i < postedNotificationCount; i++) { - NotificationManagerCompat.from(this).cancel(i); - } - postedNotificationCount = notifications.length; + return preset.buildNotifications(this, options); } @Override @@ -272,17 +287,14 @@ public class MainActivity extends Activity implements Handler.Callback { } @Override - public void onNothingSelected(AdapterView adapterView) { - } + public void onNothingSelected(AdapterView adapterView) {} } private class UpdateNotificationsOnTextChangeListener implements TextWatcher { @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - public void onTextChanged(CharSequence s, int start, int before, int count) { - } + public void onTextChanged(CharSequence s, int start, int before, int count) {} @Override public void afterTextChanged(Editable s) { diff --git a/wearable/wear/Notifications/Application/src/main/res/drawable/selected_background.xml b/wearable/wear/Notifications/Application/src/main/res/drawable/selected_background.xml index 5852dd89..f470fc8a 100644 --- a/wearable/wear/Notifications/Application/src/main/res/drawable/selected_background.xml +++ b/wearable/wear/Notifications/Application/src/main/res/drawable/selected_background.xml @@ -17,11 +17,11 @@ + android:top="@dimen/shape_padding" + android:bottom="@dimen/shape_padding" + android:left="@dimen/shape_padding" + android:right="@dimen/shape_padding"/> + android:width="@dimen/shape_stroke_width" + android:color="@color/blue" /> diff --git a/wearable/wear/Notifications/Application/src/main/res/drawable/unselected_background.xml b/wearable/wear/Notifications/Application/src/main/res/drawable/unselected_background.xml index 16131675..a5468972 100644 --- a/wearable/wear/Notifications/Application/src/main/res/drawable/unselected_background.xml +++ b/wearable/wear/Notifications/Application/src/main/res/drawable/unselected_background.xml @@ -17,11 +17,11 @@ + android:top="@dimen/shape_padding" + android:bottom="@dimen/shape_padding" + android:left="@dimen/shape_padding" + android:right="@dimen/shape_padding"/> + android:width="@dimen/shape_stroke_width" + android:color="@android:color/black" /> diff --git a/wearable/wear/Notifications/Application/src/main/res/layout/activity_main.xml b/wearable/wear/Notifications/Application/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..4304150d --- /dev/null +++ b/wearable/wear/Notifications/Application/src/main/res/layout/activity_main.xml @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wearable/wear/Notifications/Application/src/main/res/layout/main.xml b/wearable/wear/Notifications/Application/src/main/res/layout/main.xml deleted file mode 100644 index 3068ddcd..00000000 --- a/wearable/wear/Notifications/Application/src/main/res/layout/main.xml +++ /dev/null @@ -1,173 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/wearable/wear/Notifications/Application/src/main/res/values/colors.xml b/wearable/wear/Notifications/Application/src/main/res/values/colors.xml index fbcf9562..118f3d47 100644 --- a/wearable/wear/Notifications/Application/src/main/res/values/colors.xml +++ b/wearable/wear/Notifications/Application/src/main/res/values/colors.xml @@ -15,5 +15,6 @@ --> - @android:color/holo_blue_dark + #0000FF + @color/blue diff --git a/wearable/wear/Notifications/Application/src/main/res/values/dimens.xml b/wearable/wear/Notifications/Application/src/main/res/values/dimens.xml index fd97910a..77da63e4 100644 --- a/wearable/wear/Notifications/Application/src/main/res/values/dimens.xml +++ b/wearable/wear/Notifications/Application/src/main/res/values/dimens.xml @@ -26,4 +26,6 @@ 48dp + 4dp + 4dp diff --git a/wearable/wear/Notifications/Wearable/src/main/res/layout/activity_notification_display.xml b/wearable/wear/Notifications/Wearable/src/main/res/layout/activity_notification_display.xml index 7a329d2b..87074997 100644 --- a/wearable/wear/Notifications/Wearable/src/main/res/layout/activity_notification_display.xml +++ b/wearable/wear/Notifications/Wearable/src/main/res/layout/activity_notification_display.xml @@ -25,8 +25,8 @@ android:textSize="16sp" android:padding="8dp" android:gravity="center_vertical|center_horizontal" - android:textColor="#7777FF" - android:shadowColor="#222299" + android:textColor="@color/notification_display_text_color" + android:shadowColor="@color/notification_display_shadow_color" android:shadowRadius="2" android:shadowDx="1" android:shadowDy="1" diff --git a/wearable/wear/Notifications/Wearable/src/main/res/values/colors.xml b/wearable/wear/Notifications/Wearable/src/main/res/values/colors.xml index 10fad66c..a81e1bc2 100644 --- a/wearable/wear/Notifications/Wearable/src/main/res/values/colors.xml +++ b/wearable/wear/Notifications/Wearable/src/main/res/values/colors.xml @@ -18,4 +18,6 @@ #2878ff #c1c1c1 #434343 + #7777FF + #222299 -- GitLab From 922bab27fbca15c92598985ec5b4b8d3a27b0b0b Mon Sep 17 00:00:00 2001 From: Tiem Song Date: Wed, 20 Sep 2017 15:12:01 -0700 Subject: [PATCH 03/85] Add Kotlin version of Camera2Basic sample Also bump Kotlin version Bug: 66737373 Test: manually verified that there is no difference vs Java version Change-Id: I51bfdbc8aef60695f0894e12e624c146df05735a --- .../kotlinApp/Application/.gitignore | 16 + .../kotlinApp/Application/build.gradle | 28 + .../Application/src/main/AndroidManifest.xml | 40 + .../camera2basic/ActivityExtensions.kt | 34 + .../camera2basic/AutoFitTextureView.kt | 68 ++ .../camera2basic/Camera2BasicFragment.kt | 817 ++++++++++++++++++ .../android/camera2basic/CameraActivity.kt | 32 + .../camera2basic/CompareSizesByArea.kt | 17 + .../camera2basic/ConfirmationDialog.kt | 41 + .../example/android/camera2basic/Constants.kt | 22 + .../android/camera2basic/ErrorDialog.kt | 44 + .../android/camera2basic/ImageSaver.kt | 55 ++ .../main/res/drawable-hdpi/ic_action_info.png | Bin 0 -> 1025 bytes .../main/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 4251 bytes .../main/res/drawable-mdpi/ic_action_info.png | Bin 0 -> 665 bytes .../main/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 2622 bytes .../res/drawable-xhdpi/ic_action_info.png | Bin 0 -> 1355 bytes .../main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 5911 bytes .../res/drawable-xxhdpi/ic_action_info.png | Bin 0 -> 2265 bytes .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 10488 bytes .../layout-land/fragment_camera2_basic.xml | 58 ++ .../src/main/res/layout/activity_camera.xml | 22 + .../res/layout/fragment_camera2_basic.xml | 54 ++ .../src/main/res/values/colors.xml | 19 + .../src/main/res/values/dimens.xml | 5 + .../src/main/res/values/strings.xml | 32 + .../src/main/res/values/styles.xml | 20 + media/Camera2Basic/kotlinApp/CONTRIB.md | 35 + media/Camera2Basic/kotlinApp/build.gradle | 22 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 49896 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + media/Camera2Basic/kotlinApp/gradlew | 164 ++++ media/Camera2Basic/kotlinApp/gradlew.bat | 90 ++ .../kotlinApp/screenshots/icon-web.png | Bin 0 -> 64937 bytes .../kotlinApp/screenshots/main.png | Bin 0 -> 1212538 bytes media/Camera2Basic/kotlinApp/settings.gradle | 1 + 36 files changed, 1742 insertions(+) create mode 100644 media/Camera2Basic/kotlinApp/Application/.gitignore create mode 100644 media/Camera2Basic/kotlinApp/Application/build.gradle create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/AndroidManifest.xml create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/ActivityExtensions.kt create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/AutoFitTextureView.kt create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.kt create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/CameraActivity.kt create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/CompareSizesByArea.kt create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/ConfirmationDialog.kt create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/Constants.kt create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/ErrorDialog.kt create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/ImageSaver.kt create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/res/drawable-hdpi/ic_action_info.png create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/res/drawable-hdpi/ic_launcher.png create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/res/drawable-mdpi/ic_action_info.png create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/res/drawable-mdpi/ic_launcher.png create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/res/drawable-xhdpi/ic_action_info.png create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/res/drawable-xhdpi/ic_launcher.png create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/res/drawable-xxhdpi/ic_action_info.png create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/res/layout-land/fragment_camera2_basic.xml create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/res/layout/activity_camera.xml create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/res/layout/fragment_camera2_basic.xml create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/res/values/colors.xml create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/res/values/dimens.xml create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/res/values/strings.xml create mode 100644 media/Camera2Basic/kotlinApp/Application/src/main/res/values/styles.xml create mode 100644 media/Camera2Basic/kotlinApp/CONTRIB.md create mode 100644 media/Camera2Basic/kotlinApp/build.gradle create mode 100644 media/Camera2Basic/kotlinApp/gradle/wrapper/gradle-wrapper.jar create mode 100644 media/Camera2Basic/kotlinApp/gradle/wrapper/gradle-wrapper.properties create mode 100755 media/Camera2Basic/kotlinApp/gradlew create mode 100644 media/Camera2Basic/kotlinApp/gradlew.bat create mode 100644 media/Camera2Basic/kotlinApp/screenshots/icon-web.png create mode 100644 media/Camera2Basic/kotlinApp/screenshots/main.png create mode 100644 media/Camera2Basic/kotlinApp/settings.gradle diff --git a/media/Camera2Basic/kotlinApp/Application/.gitignore b/media/Camera2Basic/kotlinApp/Application/.gitignore new file mode 100644 index 00000000..6eb878d4 --- /dev/null +++ b/media/Camera2Basic/kotlinApp/Application/.gitignore @@ -0,0 +1,16 @@ +# Copyright 2013 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. +src/template/ +src/common/ +build.gradle diff --git a/media/Camera2Basic/kotlinApp/Application/build.gradle b/media/Camera2Basic/kotlinApp/Application/build.gradle new file mode 100644 index 00000000..c30a09af --- /dev/null +++ b/media/Camera2Basic/kotlinApp/Application/build.gradle @@ -0,0 +1,28 @@ +apply plugin: 'com.android.application' + +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 26 + buildToolsVersion '26.0.2' + defaultConfig { + applicationId "com.example.android.camera2basic" + minSdkVersion 21 + targetSdkVersion 26 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation "com.android.support:appcompat-v7:26.1.0" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" +} diff --git a/media/Camera2Basic/kotlinApp/Application/src/main/AndroidManifest.xml b/media/Camera2Basic/kotlinApp/Application/src/main/AndroidManifest.xml new file mode 100644 index 00000000..5b0b5b04 --- /dev/null +++ b/media/Camera2Basic/kotlinApp/Application/src/main/AndroidManifest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/ActivityExtensions.kt b/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/ActivityExtensions.kt new file mode 100644 index 00000000..14dcfa70 --- /dev/null +++ b/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/ActivityExtensions.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017 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 com.example.android.camera2basic + +import android.support.v4.app.FragmentActivity +import android.support.v4.content.ContextCompat +import android.widget.Toast + +/** + * This file illustrates Kotlin's Extension Functions by extending FragmentActivity. + */ + +/** + * Shows a [Toast] on the UI thread. + * + * @param text The message to show + */ +fun FragmentActivity.showToast(text: String) { + runOnUiThread { Toast.makeText(this, text, Toast.LENGTH_SHORT).show() } +} diff --git a/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/AutoFitTextureView.kt b/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/AutoFitTextureView.kt new file mode 100644 index 00000000..8214557f --- /dev/null +++ b/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/AutoFitTextureView.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2017 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 com.example.android.camera2basic + +import android.content.Context +import android.util.AttributeSet +import android.view.TextureView +import android.view.View + +/** + * A [TextureView] that can be adjusted to a specified aspect ratio. + */ +class AutoFitTextureView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : TextureView(context, attrs, defStyle) { + + private var ratioWidth = 0 + private var ratioHeight = 0 + + /** + * Sets the aspect ratio for this view. The size of the view will be measured based on the ratio + * calculated from the parameters. Note that the actual sizes of parameters don't matter, that + * is, calling setAspectRatio(2, 3) and setAspectRatio(4, 6) make the same result. + * + * @param width Relative horizontal size + * @param height Relative vertical size + */ + fun setAspectRatio(width: Int, height: Int) { + if (width < 0 || height < 0) { + throw IllegalArgumentException("Size cannot be negative.") + } + ratioWidth = width + ratioHeight = height + requestLayout() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val width = View.MeasureSpec.getSize(widthMeasureSpec) + val height = View.MeasureSpec.getSize(heightMeasureSpec) + if (ratioWidth == 0 || ratioHeight == 0) { + setMeasuredDimension(width, height) + } else { + if (width < height * ratioWidth / ratioHeight) { + setMeasuredDimension(width, width * ratioHeight / ratioWidth) + } else { + setMeasuredDimension(height * ratioWidth / ratioHeight, height) + } + } + } + +} diff --git a/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.kt b/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.kt new file mode 100644 index 00000000..c55df1b9 --- /dev/null +++ b/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.kt @@ -0,0 +1,817 @@ +/* + * Copyright 2017 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 com.example.android.camera2basic + +import android.Manifest +import android.app.AlertDialog +import android.content.Context +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.graphics.ImageFormat +import android.graphics.Matrix +import android.graphics.Point +import android.graphics.RectF +import android.graphics.SurfaceTexture +import android.hardware.camera2.CameraAccessException +import android.hardware.camera2.CameraCaptureSession +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.CameraDevice +import android.hardware.camera2.CameraManager +import android.hardware.camera2.CameraMetadata +import android.hardware.camera2.CaptureRequest +import android.hardware.camera2.CaptureResult +import android.hardware.camera2.TotalCaptureResult +import android.media.ImageReader +import android.os.Bundle +import android.os.Handler +import android.os.HandlerThread +import android.support.v4.app.ActivityCompat +import android.support.v4.app.Fragment +import android.support.v4.content.ContextCompat +import android.util.Log +import android.util.Size +import android.util.SparseIntArray +import android.view.LayoutInflater +import android.view.Surface +import android.view.TextureView +import android.view.View +import android.view.ViewGroup +import java.io.File +import java.util.Arrays +import java.util.Collections +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit +import kotlin.collections.ArrayList + +class Camera2BasicFragment : Fragment(), View.OnClickListener, + ActivityCompat.OnRequestPermissionsResultCallback { + + /** + * [TextureView.SurfaceTextureListener] handles several lifecycle events on a + * [TextureView]. + */ + private val surfaceTextureListener = object : TextureView.SurfaceTextureListener { + + override fun onSurfaceTextureAvailable(texture: SurfaceTexture, width: Int, height: Int) { + openCamera(width, height) + } + + override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) { + configureTransform(width, height) + } + + override fun onSurfaceTextureDestroyed(texture: SurfaceTexture) = true + + override fun onSurfaceTextureUpdated(texture: SurfaceTexture) = Unit + + } + + /** + * ID of the current [CameraDevice]. + */ + private lateinit var cameraId: String + + /** + * An [AutoFitTextureView] for camera preview. + */ + private lateinit var textureView: AutoFitTextureView + + /** + * A [CameraCaptureSession] for camera preview. + */ + private var captureSession: CameraCaptureSession? = null + + /** + * A reference to the opened [CameraDevice]. + */ + private var cameraDevice: CameraDevice? = null + + /** + * The [android.util.Size] of camera preview. + */ + private lateinit var previewSize: Size + + /** + * [CameraDevice.StateCallback] is called when [CameraDevice] changes its state. + */ + private val stateCallback = object : CameraDevice.StateCallback() { + + override fun onOpened(cameraDevice: CameraDevice) { + cameraOpenCloseLock.release() + this@Camera2BasicFragment.cameraDevice = cameraDevice + createCameraPreviewSession() + } + + override fun onDisconnected(cameraDevice: CameraDevice) { + cameraOpenCloseLock.release() + cameraDevice.close() + this@Camera2BasicFragment.cameraDevice = null + } + + override fun onError(cameraDevice: CameraDevice, error: Int) { + onDisconnected(cameraDevice) + this@Camera2BasicFragment.activity?.finish() + } + + } + + /** + * An additional thread for running tasks that shouldn't block the UI. + */ + private var backgroundThread: HandlerThread? = null + + /** + * A [Handler] for running tasks in the background. + */ + private var backgroundHandler: Handler? = null + + /** + * An [ImageReader] that handles still image capture. + */ + private var imageReader: ImageReader? = null + + /** + * This is the output file for our picture. + */ + private lateinit var file: File + + /** + * This a callback object for the [ImageReader]. "onImageAvailable" will be called when a + * still image is ready to be saved. + */ + private val onImageAvailableListener = ImageReader.OnImageAvailableListener { + backgroundHandler?.post(ImageSaver(it.acquireNextImage(), file)) + } + + /** + * [CaptureRequest.Builder] for the camera preview + */ + private lateinit var previewRequestBuilder: CaptureRequest.Builder + + /** + * [CaptureRequest] generated by [.previewRequestBuilder] + */ + private lateinit var previewRequest: CaptureRequest + + /** + * The current state of camera state for taking pictures. + * + * @see .captureCallback + */ + private var state = STATE_PREVIEW + + /** + * A [Semaphore] to prevent the app from exiting before closing the camera. + */ + private val cameraOpenCloseLock = Semaphore(1) + + /** + * Whether the current camera device supports Flash or not. + */ + private var flashSupported = false + + /** + * Orientation of the camera sensor + */ + private var sensorOrientation = 0 + + /** + * A [CameraCaptureSession.CaptureCallback] that handles events related to JPEG capture. + */ + private val captureCallback = object : CameraCaptureSession.CaptureCallback() { + + private fun process(result: CaptureResult) { + when (state) { + STATE_PREVIEW -> Unit // Do nothing when the camera preview is working normally. + STATE_WAITING_LOCK -> capturePicture(result) + STATE_WAITING_PRECAPTURE -> { + // CONTROL_AE_STATE can be null on some devices + val aeState = result.get(CaptureResult.CONTROL_AE_STATE) + if (aeState == null || + aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE || + aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED) { + state = STATE_WAITING_NON_PRECAPTURE + } + } + STATE_WAITING_NON_PRECAPTURE -> { + // CONTROL_AE_STATE can be null on some devices + val aeState = result.get(CaptureResult.CONTROL_AE_STATE) + if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { + state = STATE_PICTURE_TAKEN + captureStillPicture() + } + } + } + } + + private fun capturePicture(result: CaptureResult) { + val afState = result.get(CaptureResult.CONTROL_AF_STATE) + if (afState == null) { + captureStillPicture() + } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED + || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + // CONTROL_AE_STATE can be null on some devices + val aeState = result.get(CaptureResult.CONTROL_AE_STATE) + if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { + state = STATE_PICTURE_TAKEN + captureStillPicture() + } else { + runPrecaptureSequence() + } + } + } + + override fun onCaptureProgressed(session: CameraCaptureSession, + request: CaptureRequest, + partialResult: CaptureResult) { + process(partialResult) + } + + override fun onCaptureCompleted(session: CameraCaptureSession, + request: CaptureRequest, + result: TotalCaptureResult) { + process(result) + } + + } + + override fun onCreateView(inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_camera2_basic, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + view.findViewById(R.id.picture).setOnClickListener(this) + view.findViewById(R.id.info).setOnClickListener(this) + textureView = view.findViewById(R.id.texture) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + file = File(activity.getExternalFilesDir(null), PIC_FILE_NAME) + } + + override fun onResume() { + super.onResume() + startBackgroundThread() + + // When the screen is turned off and turned back on, the SurfaceTexture is already + // available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open + // a camera and start preview from here (otherwise, we wait until the surface is ready in + // the SurfaceTextureListener). + if (textureView.isAvailable) { + openCamera(textureView.width, textureView.height) + } else { + textureView.surfaceTextureListener = surfaceTextureListener + } + } + + override fun onPause() { + closeCamera() + stopBackgroundThread() + super.onPause() + } + + private fun requestCameraPermission() { + if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) { + ConfirmationDialog().show(childFragmentManager, FRAGMENT_DIALOG) + } else { + requestPermissions(arrayOf(Manifest.permission.CAMERA), REQUEST_CAMERA_PERMISSION) + } + } + + override fun onRequestPermissionsResult(requestCode: Int, + permissions: Array, + grantResults: IntArray) { + if (requestCode == REQUEST_CAMERA_PERMISSION) { + if (grantResults.size != 1 || grantResults[0] != PackageManager.PERMISSION_GRANTED) { + ErrorDialog.newInstance(getString(R.string.request_permission)) + .show(childFragmentManager, FRAGMENT_DIALOG) + } + } else { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + } + + /** + * Sets up member variables related to camera. + * + * @param width The width of available size for camera preview + * @param height The height of available size for camera preview + */ + private fun setUpCameraOutputs(width: Int, height: Int) { + val manager = activity.getSystemService(Context.CAMERA_SERVICE) as CameraManager + try { + for (cameraId in manager.cameraIdList) { + val characteristics = manager.getCameraCharacteristics(cameraId) + + // We don't use a front facing camera in this sample. + val cameraDirection = characteristics.get(CameraCharacteristics.LENS_FACING) + if (cameraDirection != null && + cameraDirection == CameraCharacteristics.LENS_FACING_FRONT) { + continue + } + + val map = characteristics.get( + CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) ?: continue + + // For still image captures, we use the largest available size. + val largest = Collections.max( + Arrays.asList(*map.getOutputSizes(ImageFormat.JPEG)), + CompareSizesByArea()) + imageReader = ImageReader.newInstance(largest.width, largest.height, + ImageFormat.JPEG, /*maxImages*/ 2).apply { + setOnImageAvailableListener(onImageAvailableListener, backgroundHandler) + } + + // Find out if we need to swap dimension to get the preview size relative to sensor + // coordinate. + val displayRotation = activity.windowManager.defaultDisplay.rotation + + sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) + val swappedDimensions = areDimensionsSwapped(displayRotation) + + val displaySize = Point() + activity.windowManager.defaultDisplay.getSize(displaySize) + val rotatedPreviewWidth = if (swappedDimensions) height else width + val rotatedPreviewHeight = if (swappedDimensions) width else height + var maxPreviewWidth = if (swappedDimensions) displaySize.y else displaySize.x + var maxPreviewHeight = if (swappedDimensions) displaySize.x else displaySize.y + + if (maxPreviewWidth > MAX_PREVIEW_WIDTH) maxPreviewWidth = MAX_PREVIEW_WIDTH + if (maxPreviewHeight > MAX_PREVIEW_HEIGHT) maxPreviewHeight = MAX_PREVIEW_HEIGHT + + // Danger, W.R.! Attempting to use too large a preview size could exceed the camera + // bus' bandwidth limitation, resulting in gorgeous previews but the storage of + // garbage capture data. + previewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture::class.java), + rotatedPreviewWidth, rotatedPreviewHeight, + maxPreviewWidth, maxPreviewHeight, + largest) + + // We fit the aspect ratio of TextureView to the size of preview we picked. + if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { + textureView.setAspectRatio(previewSize.width, previewSize.height) + } else { + textureView.setAspectRatio(previewSize.height, previewSize.width) + } + + // Check if the flash is supported. + flashSupported = + characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) == true + + this.cameraId = cameraId + + // We've found a viable camera and finished setting up member variables, + // so we don't need to iterate through other available cameras. + return + } + } catch (e: CameraAccessException) { + Log.e(TAG, e.toString()) + } catch (e: NullPointerException) { + // Currently an NPE is thrown when the Camera2API is used but not supported on the + // device this code runs. + ErrorDialog.newInstance(getString(R.string.camera_error)) + .show(childFragmentManager, FRAGMENT_DIALOG) + } + + } + + /** + * Determines if the dimensions are swapped given the phone's current rotation. + * + * @param displayRotation The current rotation of the display + * + * @return true if the dimensions are swapped, false otherwise. + */ + private fun areDimensionsSwapped(displayRotation: Int): Boolean { + var swappedDimensions = false + when (displayRotation) { + Surface.ROTATION_0, Surface.ROTATION_180 -> { + if (sensorOrientation == 90 || sensorOrientation == 270) { + swappedDimensions = true + } + } + Surface.ROTATION_90, Surface.ROTATION_270 -> { + if (sensorOrientation == 0 || sensorOrientation == 180) { + swappedDimensions = true + } + } + else -> { + Log.e(TAG, "Display rotation is invalid: $displayRotation") + } + } + return swappedDimensions + } + + /** + * Opens the camera specified by [Camera2BasicFragment.cameraId]. + */ + private fun openCamera(width: Int, height: Int) { + val permission = ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) + if (permission != PackageManager.PERMISSION_GRANTED) { + requestCameraPermission() + return + } + setUpCameraOutputs(width, height) + configureTransform(width, height) + val manager = activity.getSystemService(Context.CAMERA_SERVICE) as CameraManager + try { + // Wait for camera to open - 2.5 seconds is sufficient + if (!cameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) { + throw RuntimeException("Time out waiting to lock camera opening.") + } + manager.openCamera(cameraId, stateCallback, backgroundHandler) + } catch (e: CameraAccessException) { + Log.e(TAG, e.toString()) + } catch (e: InterruptedException) { + throw RuntimeException("Interrupted while trying to lock camera opening.", e) + } + + } + + /** + * Closes the current [CameraDevice]. + */ + private fun closeCamera() { + try { + cameraOpenCloseLock.acquire() + captureSession?.close() + captureSession = null + cameraDevice?.close() + cameraDevice = null + imageReader?.close() + imageReader = null + } catch (e: InterruptedException) { + throw RuntimeException("Interrupted while trying to lock camera closing.", e) + } finally { + cameraOpenCloseLock.release() + } + } + + /** + * Starts a background thread and its [Handler]. + */ + private fun startBackgroundThread() { + backgroundThread = HandlerThread("CameraBackground").also { it.start() } + backgroundHandler = Handler(backgroundThread?.looper) + } + + /** + * Stops the background thread and its [Handler]. + */ + private fun stopBackgroundThread() { + backgroundThread?.quitSafely() + try { + backgroundThread?.join() + backgroundThread = null + backgroundHandler = null + } catch (e: InterruptedException) { + Log.e(TAG, e.toString()) + } + + } + + /** + * Creates a new [CameraCaptureSession] for camera preview. + */ + private fun createCameraPreviewSession() { + try { + val texture = textureView.surfaceTexture + + // We configure the size of default buffer to be the size of camera preview we want. + texture.setDefaultBufferSize(previewSize.width, previewSize.height) + + // This is the output Surface we need to start preview. + val surface = Surface(texture) + + // We set up a CaptureRequest.Builder with the output Surface. + previewRequestBuilder = cameraDevice!!.createCaptureRequest( + CameraDevice.TEMPLATE_PREVIEW + ) + previewRequestBuilder.addTarget(surface) + + // Here, we create a CameraCaptureSession for camera preview. + cameraDevice?.createCaptureSession(Arrays.asList(surface, imageReader?.surface), + object : CameraCaptureSession.StateCallback() { + + override fun onConfigured(cameraCaptureSession: CameraCaptureSession) { + // The camera is already closed + if (cameraDevice == null) return + + // When the session is ready, we start displaying the preview. + captureSession = cameraCaptureSession + try { + // Auto focus should be continuous for camera preview. + previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, + CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) + // Flash is automatically enabled when necessary. + setAutoFlash(previewRequestBuilder) + + // Finally, we start displaying the camera preview. + previewRequest = previewRequestBuilder.build() + captureSession?.setRepeatingRequest(previewRequest, + captureCallback, backgroundHandler) + } catch (e: CameraAccessException) { + Log.e(TAG, e.toString()) + } + + } + + override fun onConfigureFailed(session: CameraCaptureSession) { + activity.showToast("Failed") + } + }, null) + } catch (e: CameraAccessException) { + Log.e(TAG, e.toString()) + } + + } + + /** + * Configures the necessary [android.graphics.Matrix] transformation to `textureView`. + * This method should be called after the camera preview size is determined in + * setUpCameraOutputs and also the size of `textureView` is fixed. + * + * @param viewWidth The width of `textureView` + * @param viewHeight The height of `textureView` + */ + private fun configureTransform(viewWidth: Int, viewHeight: Int) { + activity ?: return + val rotation = activity.windowManager.defaultDisplay.rotation + val matrix = Matrix() + val viewRect = RectF(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat()) + val bufferRect = RectF(0f, 0f, previewSize.height.toFloat(), previewSize.width.toFloat()) + val centerX = viewRect.centerX() + val centerY = viewRect.centerY() + + if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) { + bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY()) + val scale = Math.max( + viewHeight.toFloat() / previewSize.height, + viewWidth.toFloat() / previewSize.width) + with(matrix) { + setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL) + postScale(scale, scale, centerX, centerY) + postRotate((90 * (rotation - 2)).toFloat(), centerX, centerY) + } + } else if (Surface.ROTATION_180 == rotation) { + matrix.postRotate(180f, centerX, centerY) + } + textureView.setTransform(matrix) + } + + /** + * Lock the focus as the first step for a still image capture. + */ + private fun lockFocus() { + try { + // This is how to tell the camera to lock focus. + previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, + CameraMetadata.CONTROL_AF_TRIGGER_START) + // Tell #captureCallback to wait for the lock. + state = STATE_WAITING_LOCK + captureSession?.capture(previewRequestBuilder.build(), captureCallback, + backgroundHandler) + } catch (e: CameraAccessException) { + Log.e(TAG, e.toString()) + } + + } + + /** + * Run the precapture sequence for capturing a still image. This method should be called when + * we get a response in [.captureCallback] from [.lockFocus]. + */ + private fun runPrecaptureSequence() { + try { + // This is how to tell the camera to trigger. + previewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) + // Tell #captureCallback to wait for the precapture sequence to be set. + state = STATE_WAITING_PRECAPTURE + captureSession?.capture(previewRequestBuilder.build(), captureCallback, + backgroundHandler) + } catch (e: CameraAccessException) { + Log.e(TAG, e.toString()) + } + + } + + /** + * Capture a still picture. This method should be called when we get a response in + * [.captureCallback] from both [.lockFocus]. + */ + private fun captureStillPicture() { + try { + if (activity == null || cameraDevice == null) return + val rotation = activity.windowManager.defaultDisplay.rotation + + // This is the CaptureRequest.Builder that we use to take a picture. + val captureBuilder = cameraDevice?.createCaptureRequest( + CameraDevice.TEMPLATE_STILL_CAPTURE)?.apply { + addTarget(imageReader?.surface) + + // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X) + // We have to take that into account and rotate JPEG properly. + // For devices with orientation of 90, we return our mapping from ORIENTATIONS. + // For devices with orientation of 270, we need to rotate the JPEG 180 degrees. + set(CaptureRequest.JPEG_ORIENTATION, + (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360) + + // Use the same AE and AF modes as the preview. + set(CaptureRequest.CONTROL_AF_MODE, + CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) + }?.also { setAutoFlash(it) } + + val captureCallback = object : CameraCaptureSession.CaptureCallback() { + + override fun onCaptureCompleted(session: CameraCaptureSession, + request: CaptureRequest, + result: TotalCaptureResult) { + activity.showToast("Saved: $file") + Log.d(TAG, file.toString()) + unlockFocus() + } + } + + captureSession?.apply { + stopRepeating() + abortCaptures() + capture(captureBuilder?.build(), captureCallback, null) + } + } catch (e: CameraAccessException) { + Log.e(TAG, e.toString()) + } + + } + + /** + * Unlock the focus. This method should be called when still image capture sequence is + * finished. + */ + private fun unlockFocus() { + try { + // Reset the auto-focus trigger + previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, + CameraMetadata.CONTROL_AF_TRIGGER_CANCEL) + setAutoFlash(previewRequestBuilder) + captureSession?.capture(previewRequestBuilder.build(), captureCallback, + backgroundHandler) + // After this, the camera will go back to the normal state of preview. + state = STATE_PREVIEW + captureSession?.setRepeatingRequest(previewRequest, captureCallback, + backgroundHandler) + } catch (e: CameraAccessException) { + Log.e(TAG, e.toString()) + } + + } + + override fun onClick(view: View) { + when (view.id) { + R.id.picture -> lockFocus() + R.id.info -> { + if (activity != null) { + AlertDialog.Builder(activity) + .setMessage(R.string.intro_message) + .setPositiveButton(android.R.string.ok, null) + .show() + } + } + } + } + + private fun setAutoFlash(requestBuilder: CaptureRequest.Builder) { + if (flashSupported) { + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH) + } + } + + companion object { + + /** + * Conversion from screen rotation to JPEG orientation. + */ + private val ORIENTATIONS = SparseIntArray() + private val FRAGMENT_DIALOG = "dialog" + + init { + ORIENTATIONS.append(Surface.ROTATION_0, 90) + ORIENTATIONS.append(Surface.ROTATION_90, 0) + ORIENTATIONS.append(Surface.ROTATION_180, 270) + ORIENTATIONS.append(Surface.ROTATION_270, 180) + } + + /** + * Tag for the [Log]. + */ + private val TAG = "Camera2BasicFragment" + + /** + * Camera state: Showing camera preview. + */ + private val STATE_PREVIEW = 0 + + /** + * Camera state: Waiting for the focus to be locked. + */ + private val STATE_WAITING_LOCK = 1 + + /** + * Camera state: Waiting for the exposure to be precapture state. + */ + private val STATE_WAITING_PRECAPTURE = 2 + + /** + * Camera state: Waiting for the exposure state to be something other than precapture. + */ + private val STATE_WAITING_NON_PRECAPTURE = 3 + + /** + * Camera state: Picture was taken. + */ + private val STATE_PICTURE_TAKEN = 4 + + /** + * Max preview width that is guaranteed by Camera2 API + */ + private val MAX_PREVIEW_WIDTH = 1920 + + /** + * Max preview height that is guaranteed by Camera2 API + */ + private val MAX_PREVIEW_HEIGHT = 1080 + + /** + * Given `choices` of `Size`s supported by a camera, choose the smallest one that + * is at least as large as the respective texture view size, and that is at most as large as + * the respective max size, and whose aspect ratio matches with the specified value. If such + * size doesn't exist, choose the largest one that is at most as large as the respective max + * size, and whose aspect ratio matches with the specified value. + * + * @param choices The list of sizes that the camera supports for the intended + * output class + * @param textureViewWidth The width of the texture view relative to sensor coordinate + * @param textureViewHeight The height of the texture view relative to sensor coordinate + * @param maxWidth The maximum width that can be chosen + * @param maxHeight The maximum height that can be chosen + * @param aspectRatio The aspect ratio + * @return The optimal `Size`, or an arbitrary one if none were big enough + */ + @JvmStatic private fun chooseOptimalSize( + choices: Array, + textureViewWidth: Int, + textureViewHeight: Int, + maxWidth: Int, + maxHeight: Int, + aspectRatio: Size + ): Size { + + // Collect the supported resolutions that are at least as big as the preview Surface + val bigEnough = ArrayList() + // Collect the supported resolutions that are smaller than the preview Surface + val notBigEnough = ArrayList() + val w = aspectRatio.width + val h = aspectRatio.height + for (option in choices) { + if (option.width <= maxWidth && option.height <= maxHeight && + option.height == option.width * h / w) { + if (option.width >= textureViewWidth && option.height >= textureViewHeight) { + bigEnough.add(option) + } else { + notBigEnough.add(option) + } + } + } + + // Pick the smallest of those big enough. If there is no one big enough, pick the + // largest of those not big enough. + if (bigEnough.size > 0) { + return Collections.min(bigEnough, CompareSizesByArea()) + } else if (notBigEnough.size > 0) { + return Collections.max(notBigEnough, CompareSizesByArea()) + } else { + Log.e(TAG, "Couldn't find any suitable preview size") + return choices[0] + } + } + + @JvmStatic fun newInstance(): Camera2BasicFragment = Camera2BasicFragment() + } +} diff --git a/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/CameraActivity.kt b/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/CameraActivity.kt new file mode 100644 index 00000000..e0e85fb0 --- /dev/null +++ b/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/CameraActivity.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2017 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 com.example.android.camera2basic + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity + +class CameraActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_camera) + savedInstanceState ?: supportFragmentManager.beginTransaction() + .replace(R.id.container, Camera2BasicFragment.newInstance()) + .commit() + } + +} diff --git a/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/CompareSizesByArea.kt b/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/CompareSizesByArea.kt new file mode 100644 index 00000000..83cc2f11 --- /dev/null +++ b/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/CompareSizesByArea.kt @@ -0,0 +1,17 @@ +package com.example.android.camera2basic + +import android.util.Size +import java.lang.Long.signum + +import java.util.Comparator + +/** + * Compares two `Size`s based on their areas. + */ +internal class CompareSizesByArea : Comparator { + + // We cast here to ensure the multiplications won't overflow + override fun compare(lhs: Size, rhs: Size) = + signum(lhs.width.toLong() * lhs.height - rhs.width.toLong() * rhs.height) + +} diff --git a/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/ConfirmationDialog.kt b/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/ConfirmationDialog.kt new file mode 100644 index 00000000..f276c737 --- /dev/null +++ b/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/ConfirmationDialog.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2017 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 com.example.android.camera2basic + +import android.Manifest +import android.app.AlertDialog +import android.app.Dialog +import android.os.Bundle +import android.support.v4.app.DialogFragment + +/** + * Shows OK/Cancel confirmation dialog about camera permission. + */ +class ConfirmationDialog : DialogFragment() { + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = + AlertDialog.Builder(activity) + .setMessage(R.string.request_permission) + .setPositiveButton(android.R.string.ok) { _, _ -> + parentFragment.requestPermissions(arrayOf(Manifest.permission.CAMERA), + REQUEST_CAMERA_PERMISSION) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + parentFragment.activity?.finish() + } + .create() +} diff --git a/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/Constants.kt b/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/Constants.kt new file mode 100644 index 00000000..0febbd94 --- /dev/null +++ b/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/Constants.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2017 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. + */ + +@file:JvmName("Constants") + +package com.example.android.camera2basic + +@JvmField val REQUEST_CAMERA_PERMISSION = 1 +@JvmField val PIC_FILE_NAME = "pic.jpg" diff --git a/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/ErrorDialog.kt b/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/ErrorDialog.kt new file mode 100644 index 00000000..16ecceb7 --- /dev/null +++ b/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/ErrorDialog.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2017 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 com.example.android.camera2basic + +import android.app.AlertDialog +import android.app.Dialog +import android.os.Bundle +import android.support.v4.app.DialogFragment + +/** + * Shows an error message dialog. + */ +class ErrorDialog : DialogFragment() { + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = + AlertDialog.Builder(activity) + .setMessage(arguments.getString(ARG_MESSAGE)) + .setPositiveButton(android.R.string.ok) { _, _ -> activity.finish() } + .create() + + companion object { + + @JvmStatic private val ARG_MESSAGE = "message" + + @JvmStatic fun newInstance(message: String): ErrorDialog = ErrorDialog().apply { + arguments = Bundle().apply { putString(ARG_MESSAGE, message) } + } + } + +} \ No newline at end of file diff --git a/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/ImageSaver.kt b/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/ImageSaver.kt new file mode 100644 index 00000000..bf33d4b3 --- /dev/null +++ b/media/Camera2Basic/kotlinApp/Application/src/main/java/com/example/android/camera2basic/ImageSaver.kt @@ -0,0 +1,55 @@ +package com.example.android.camera2basic + +import android.media.Image +import android.util.Log + +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.nio.ByteBuffer + +/** + * Saves a JPEG [Image] into the specified [File]. + */ +internal class ImageSaver( + /** + * The JPEG image + */ + private val image: Image, + + /** + * The file we save the image into. + */ + private val file: File +) : Runnable { + + override fun run() { + val buffer = image.planes[0].buffer + val bytes = ByteArray(buffer.remaining()) + buffer.get(bytes) + var output: FileOutputStream? = null + try { + output = FileOutputStream(file).apply { + write(bytes) + } + } catch (e: IOException) { + Log.e(TAG, e.toString()) + } finally { + image.close() + output?.let { + try { + it.close() + } catch (e: IOException) { + Log.e(TAG, e.toString()) + } + } + } + } + + companion object { + /** + * Tag for the [Log]. + */ + private val TAG = "ImageSaver" + } +} diff --git a/media/Camera2Basic/kotlinApp/Application/src/main/res/drawable-hdpi/ic_action_info.png b/media/Camera2Basic/kotlinApp/Application/src/main/res/drawable-hdpi/ic_action_info.png new file mode 100644 index 0000000000000000000000000000000000000000..32bd1aabcabb85ded957230533c00e735183a323 GIT binary patch literal 1025 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 literal 0 HcmV?d00001 diff --git a/media/Camera2Basic/kotlinApp/Application/src/main/res/drawable-mdpi/ic_launcher.png b/media/Camera2Basic/kotlinApp/Application/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..65f92a5227de9da5da0eb504606e85934bc9b94c GIT binary patch literal 2622 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 + + + + + + +