Messaging: Vendor chips, colorpicker, photoviewer, and vcard in-tree

Clone no longer depends on LineageOS gitlinks; the libraries used by the app live in this repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-29 06:19:53 +09:00
co-authored by Cursor
parent a6b23f0f3c
commit 49789fc4a5
239 changed files with 35551 additions and 4 deletions
Submodule modules/photoviewer deleted from fb5c100b4a
+7
View File
@@ -0,0 +1,7 @@
*~
*.bak
*.class
bin/
gen/
.classpath
.project
+74
View File
@@ -0,0 +1,74 @@
// Copyright (C) 2012 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.
//#################################################
// Build appcompat library
package {
default_applicable_licenses: ["Android-Apache-2.0"],
}
android_library {
name: "libphotoviewer_appcompat",
libs: ["androidx.annotation_annotation"],
static_libs: [
"androidx.core_core",
"androidx.legacy_legacy-support-core-ui",
"androidx.legacy_legacy-support-core-utils",
"androidx.fragment_fragment",
"androidx.appcompat_appcompat",
],
sdk_version: "current",
srcs: [
"src/**/*.java",
"appcompat/src/**/*.java",
"src/**/*.logtags",
],
resource_dirs: [
"appcompat/res",
"res",
],
}
//#################################################
// Build non-appcompat library
android_library {
name: "libphotoviewer",
libs: ["androidx.annotation_annotation"],
static_libs: [
"androidx.core_core",
"androidx.legacy_legacy-support-core-ui",
"androidx.legacy_legacy-support-core-utils",
"androidx.fragment_fragment",
],
sdk_version: "current",
srcs: [
"src/**/*.java",
"activity/src/**/*.java",
"src/**/*.logtags",
],
resource_dirs: [
"activity/res",
"res",
]
}
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2012 Google Inc.
Licensed to 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.ex.photo"
android:versionCode="1">
<application />
</manifest>
+1
View File
@@ -0,0 +1 @@
asapperstein@google.com
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2012 Google Inc.
Licensed to 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.
-->
<resources>
<style name="PhotoViewTheme" parent="android:Theme.Holo">
<item name="android:colorPrimaryDark">@color/photo_status_bar_black</item>
<item name="android:navigationBarColor">@color/photo_status_bar_black</item>
<item name="android:windowNoTitle">false</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowActionBarOverlay">true</item>
<item name="android:windowBackground">@color/solid_black</item>
<item name="android:actionBarStyle">@style/Holo.ActionBar</item>
</style>
<style name="PhotoViewTheme.Translucent">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
</style>
<style name="PhotoViewTheme.NoActionBar">
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
</style>
<style name="Holo.ActionBar" parent="android:Widget.Holo.ActionBar">
<item name="android:background">@drawable/actionbar_translucent</item>
</style>
</resources>
@@ -0,0 +1,76 @@
package com.android.ex.photo;
import android.app.ActionBar;
import android.graphics.drawable.Drawable;
/**
* Wrapper around {@link ActionBar}.
*/
public class ActionBarWrapper implements ActionBarInterface {
private final ActionBar mActionBar;
private class MenuVisiblityListenerWrapper implements ActionBar.OnMenuVisibilityListener {
private final ActionBarInterface.OnMenuVisibilityListener mWrapped;
public MenuVisiblityListenerWrapper(ActionBarInterface.OnMenuVisibilityListener wrapped) {
mWrapped = wrapped;
}
@Override
public void onMenuVisibilityChanged(boolean isVisible) {
mWrapped.onMenuVisibilityChanged(isVisible);
}
}
public ActionBarWrapper(ActionBar actionBar) {
mActionBar = actionBar;
}
@Override
public void setDisplayHomeAsUpEnabled(boolean showHomeAsUp) {
mActionBar.setDisplayHomeAsUpEnabled(showHomeAsUp);
}
@Override
public void addOnMenuVisibilityListener(OnMenuVisibilityListener listener) {
mActionBar.addOnMenuVisibilityListener(new MenuVisiblityListenerWrapper(listener));
}
@Override
public void setDisplayOptionsShowTitle() {
mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE, ActionBar.DISPLAY_SHOW_TITLE);
}
@Override
public CharSequence getTitle() {
return mActionBar.getTitle();
}
@Override
public void setTitle(CharSequence title) {
mActionBar.setTitle(title);
}
@Override
public void setSubtitle(CharSequence subtitle) {
mActionBar.setSubtitle(subtitle);
}
@Override
public void show() {
mActionBar.show();
}
@Override
public void hide() {
mActionBar.hide();
}
@Override
public void setLogo(Drawable logo) {
mActionBar.setLogo(logo);
}
}
@@ -0,0 +1,130 @@
/*
* Copyright (C) 2011 Google Inc.
* Licensed to 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.android.ex.photo;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import android.view.Menu;
import android.view.MenuItem;
/**
* Activity to view the contents of an album.
*/
public class PhotoViewActivity extends FragmentActivity
implements PhotoViewController.ActivityInterface {
private PhotoViewController mController;
private ActionBarWrapper mActionBar;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mController = createController();
mController.onCreate(savedInstanceState);
}
public PhotoViewController createController() {
return new PhotoViewController(this);
}
@Override
public PhotoViewController getController() {
return mController;
}
@Override
protected void onStart() {
super.onStart();
mController.onStart();
}
@Override
protected void onResume() {
super.onResume();
mController.onResume();
}
@Override
protected void onPause() {
mController.onPause();
super.onPause();
}
@Override
protected void onStop() {
mController.onStop();
super.onStop();
}
@Override
protected void onDestroy() {
mController.onDestroy();
super.onDestroy();
}
@Override
public void onBackPressed() {
if (!mController.onBackPressed()) {
super.onBackPressed();
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mController.onSaveInstanceState(outState);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
return mController.onCreateOptionsMenu(menu) || super.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
return mController.onPrepareOptionsMenu(menu) || super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
return mController.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
mController.onActivityResult(requestCode, resultCode, data);
}
@Override
public Context getContext() {
return this;
}
@Override
public ActionBarInterface getActionBarInterface() {
if (mActionBar == null) {
mActionBar = new ActionBarWrapper(getActionBar());
}
return mActionBar;
}
}
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2012 Google Inc.
Licensed to 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.
-->
<resources>
<style name="PhotoViewTheme" parent="Theme.AppCompat">
<item name="actionBarStyle">@style/AppCompat.ActionBar</item>
<item name="colorPrimaryDark">@color/photo_status_bar_black</item>
<item name="android:navigationBarColor">@color/photo_status_bar_black</item>
<item name="windowActionBarOverlay">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowBackground">@color/solid_black</item>
</style>
<style name="PhotoViewTheme.Translucent">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
</style>
<style name="PhotoViewTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="AppCompat.ActionBar" parent="Widget.AppCompat.ActionBar">
<item name="background">@drawable/actionbar_translucent</item>
</style>
</resources>
@@ -0,0 +1,77 @@
package com.android.ex.photo;
import android.graphics.drawable.Drawable;
import androidx.appcompat.app.ActionBar;
/**
* Wrapper around {@link ActionBar}.
*/
public class ActionBarWrapper implements ActionBarInterface {
private final ActionBar mActionBar;
private class MenuVisiblityListenerWrapper implements ActionBar.OnMenuVisibilityListener {
private final ActionBarInterface.OnMenuVisibilityListener mWrapped;
public MenuVisiblityListenerWrapper(ActionBarInterface.OnMenuVisibilityListener wrapped) {
mWrapped = wrapped;
}
@Override
public void onMenuVisibilityChanged(boolean isVisible) {
mWrapped.onMenuVisibilityChanged(isVisible);
}
}
public ActionBarWrapper(ActionBar actionBar) {
mActionBar = actionBar;
}
@Override
public void setDisplayHomeAsUpEnabled(boolean showHomeAsUp) {
mActionBar.setDisplayHomeAsUpEnabled(showHomeAsUp);
}
@Override
public void addOnMenuVisibilityListener(OnMenuVisibilityListener listener) {
mActionBar.addOnMenuVisibilityListener(new MenuVisiblityListenerWrapper(listener));
}
@Override
public void setDisplayOptionsShowTitle() {
mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE, ActionBar.DISPLAY_SHOW_TITLE);
}
@Override
public CharSequence getTitle() {
return mActionBar.getTitle();
}
@Override
public void setTitle(CharSequence title) {
mActionBar.setTitle(title);
}
@Override
public void setSubtitle(CharSequence subtitle) {
mActionBar.setSubtitle(subtitle);
}
@Override
public void show() {
mActionBar.show();
}
@Override
public void hide() {
mActionBar.hide();
}
@Override
public void setLogo(Drawable logo) {
mActionBar.setLogo(logo);
}
}
@@ -0,0 +1,130 @@
/*
* Copyright (C) 2011 Google Inc.
* Licensed to 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.android.ex.photo;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem;
/**
* Activity to view the contents of an album.
*/
public class PhotoViewActivity extends AppCompatActivity
implements PhotoViewController.ActivityInterface {
private PhotoViewController mController;
private ActionBarWrapper mActionBar;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mController = createController();
mController.onCreate(savedInstanceState);
}
protected PhotoViewController createController() {
return new PhotoViewController(this);
}
@Override
public PhotoViewController getController() {
return mController;
}
@Override
protected void onStart() {
super.onStart();
mController.onStart();
}
@Override
protected void onResume() {
super.onResume();
mController.onResume();
}
@Override
protected void onPause() {
mController.onPause();
super.onPause();
}
@Override
protected void onStop() {
mController.onStop();
super.onStop();
}
@Override
protected void onDestroy() {
mController.onDestroy();
super.onDestroy();
}
@Override
public void onBackPressed() {
if (!mController.onBackPressed()) {
super.onBackPressed();
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mController.onSaveInstanceState(outState);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
return mController.onCreateOptionsMenu(menu) || super.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
return mController.onPrepareOptionsMenu(menu) || super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
return mController.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
mController.onActivityResult(requestCode, resultCode, data);
}
@Override
public Context getContext() {
return this;
}
@Override
public ActionBarInterface getActionBarInterface() {
if (mActionBar == null) {
mActionBar = new ActionBarWrapper(getSupportActionBar());
}
return mActionBar;
}
}
+33
View File
@@ -0,0 +1,33 @@
plugins {
id 'com.android.library'
}
android {
namespace 'com.android.ex.photo'
compileSdk 35
defaultConfig {
minSdk 33
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src', 'activity/src']
res.srcDirs = ['res', 'activity/res']
}
}
}
dependencies {
implementation 'androidx.annotation:annotation:1.8.2'
implementation 'androidx.core:core:1.15.0'
implementation 'androidx.fragment:fragment:1.8.5'
implementation 'androidx.loader:loader:1.1.0'
implementation 'androidx.viewpager:viewpager:1.0.0'
}
+15
View File
@@ -0,0 +1,15 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-19
android.library=true
Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 889 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- these are the dimensions of the original default_image.png -->
<size
android:width="215dp"
android:height="210dp" />
<solid
android:color="#ccc" />
</shape>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<ImageView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/retry_button"
android:layout_width="@dimen/retry_button_size"
android:layout_height="@dimen/retry_button_size"
android:layout_below="@id/empty_text"
android:layout_centerHorizontal="true"
android:background="?android:attr/selectableItemBackground"
android:scaleType="center"
android:src="@drawable/ic_menu_refresh_holo_dark"
android:visibility="gone" />
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2011 Google Inc.
Licensed to 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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/photo_activity_root_view"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<View
android:id="@+id/photo_activity_background"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ff000000" />
<com.android.ex.photo.PhotoViewPager
android:id="@+id/photo_view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/photo_activity_temporary_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"/>
</FrameLayout>
@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2011 Google Inc.
Licensed to 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.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.android.ex.photo.views.PhotoView
android:id="@+id/photo_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<FrameLayout
android:id="@+id/photo_preview"
android:layout_width="@dimen/photo_preview_size"
android:layout_height="@dimen/photo_preview_size"
android:layout_centerInParent="true" >
<ImageView
android:id="@+id/photo_preview_image"
android:layout_width="@dimen/photo_preview_size"
android:layout_height="@dimen/photo_preview_size"
android:layout_gravity="center"
android:scaleType="centerCrop"
android:src="@drawable/default_image"
android:visibility="gone" />
<ProgressBar
android:id="@+id/indeterminate_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="@dimen/photo_preview_size"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:indeterminate="true"
android:visibility="gone" />
<ProgressBar
android:id="@+id/determinate_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="@dimen/photo_preview_size"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:indeterminate="false"
android:visibility="gone" />
</FrameLayout>
<TextView
android:id="@+id/empty_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/photo_preview"
android:layout_centerHorizontal="true"
android:layout_marginTop="8dip"
android:textColor="@android:color/white"
android:visibility="gone" />
<!-- retry button might use a background that is ICS+ so include it from the right layout -->
<include layout="@layout/photo_retry_button" />
</RelativeLayout>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<ImageView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/retry_button"
android:layout_width="@dimen/retry_button_size"
android:layout_height="@dimen/retry_button_size"
android:layout_below="@id/empty_text"
android:layout_centerHorizontal="true"
android:scaleType="center"
android:src="@drawable/ic_menu_refresh_holo_dark"
android:visibility="gone" />
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2011 Google Inc.
Licensed to 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.
-->
<resources>
<!-- Photo Viewer Colors -->
<color name="solid_black">#ff000000</color>
<color name="photo_status_bar_black">#d9000000</color>
<color name="photo_crop_dim_color">#cc000000</color>
<color name="photo_crop_highlight_color">#fff</color>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- If true, thumbnails will be forcibly set to ScaleType.CENTER -->
<bool name="force_thumbnail_no_scaling">true</bool>
</resources>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2012 Google Inc.
Licensed to 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.
-->
<resources>
<integer name="reenter_fullscreen_delay_time_in_millis">5000</integer>
</resources>
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2012 Google Inc.
Licensed to 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.
-->
<resources>
<dimen name="photo_crop_width">280dip</dimen>
<dimen name="photo_crop_stroke_width">1dip</dimen>
<dimen name="photo_preview_size">200dip</dimen>
<dimen name="retry_button_size">48dip</dimen>
<dimen name="photo_page_margin">32dip</dimen>
</resources>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2012 Google Inc.
Licensed to 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.
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- Photo view sub-title for current photo position [CHAR LIMIT=10] -->
<string name="photo_view_count"><xliff:g id="current_pos">%1$d</xliff:g> of <xliff:g id="count">%2$d</xliff:g></string>
<string name="retry">Retry</string>
<!-- Photo view error message shown below thumbnail if full image could not be shown [CHAR LIMIT=30] -->
<string name="failed">Couldn\'t load image</string>
<!-- Title and subtitle combined into a single string -->
<string name="titles"><xliff:g id="title">%1$s</xliff:g>, <xliff:g id="subtitle">%2$s</xliff:g></string>
</resources>
@@ -0,0 +1,47 @@
package com.android.ex.photo;
import android.graphics.drawable.Drawable;
import androidx.annotation.Nullable;
/**
* Wrapper activity for an action bar. This wraps either a {@link android.app.ActionBar} or
* {@link androidx.appcompat.app.ActionBar}.
*/
public interface ActionBarInterface {
public interface OnMenuVisibilityListener {
/**
* Called when an action bar menu is shown or hidden. Applications may want to use
* this to tune auto-hiding behavior for the action bar or pause/resume video playback,
* gameplay, or other activity within the main content area.
*
* @param isVisible True if an action bar menu is now visible, false if no action bar
* menus are visible.
*/
public void onMenuVisibilityChanged(boolean isVisible);
}
public void setDisplayHomeAsUpEnabled(boolean showHomeAsUp);
public void addOnMenuVisibilityListener(OnMenuVisibilityListener listener);
/**
* Wrapper for {@code setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE,
* ActionBar.DISPLAY_SHOW_TITLE)}.
*/
public void setDisplayOptionsShowTitle();
@Nullable
public CharSequence getTitle();
public void setTitle(@Nullable CharSequence title);
public void setSubtitle(@Nullable CharSequence subtitle);
public void show();
public void hide();
public void setLogo(@Nullable Drawable logo);
}
@@ -0,0 +1,351 @@
/*
* Copyright (C) 2011 Google Inc.
* Licensed to 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.android.ex.photo;
import android.app.Activity;
import android.content.ContentProvider;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import com.android.ex.photo.fragments.PhotoViewFragment;
/**
* Build intents to start app activities
*/
public class Intents {
// Intent extras
public static final String EXTRA_PHOTO_INDEX = "photo_index";
public static final String EXTRA_INITIAL_PHOTO_URI = "initial_photo_uri";
public static final String EXTRA_PHOTOS_URI = "photos_uri";
public static final String EXTRA_RESOLVED_PHOTO_URI = "resolved_photo_uri";
public static final String EXTRA_PROJECTION = "projection";
public static final String EXTRA_THUMBNAIL_URI = "thumbnail_uri";
public static final String EXTRA_CONTENT_DESCRIPTION = "content_description";
public static final String EXTRA_MAX_INITIAL_SCALE = "max_scale";
public static final String EXTRA_WATCH_NETWORK = "watch_network";
public static final String EXTRA_ENABLE_TIMER_LIGHTS_OUT = "enable_timer_lights_out";
// Parameters affecting the intro/exit animation
public static final String EXTRA_SCALE_UP_ANIMATION = "scale_up_animation";
public static final String EXTRA_ANIMATION_START_X = "start_x_extra";
public static final String EXTRA_ANIMATION_START_Y = "start_y_extra";
public static final String EXTRA_ANIMATION_START_WIDTH = "start_width_extra";
public static final String EXTRA_ANIMATION_START_HEIGHT = "start_height_extra";
// Parameters affecting the display and features
public static final String EXTRA_ACTION_BAR_HIDDEN_INITIALLY = "action_bar_hidden_initially";
public static final String EXTRA_DISPLAY_THUMBS_FULLSCREEN = "display_thumbs_fullscreen";
/**
* Gets a photo view intent builder to display the photos from phone activity.
*
* @param context The context
* @return The intent builder
*/
public static PhotoViewIntentBuilder newPhotoViewActivityIntentBuilder(Context context) {
return new PhotoViewIntentBuilder(context, PhotoViewActivity.class);
}
/**
* Gets a photo view intent builder to display the photo view fragment
*
* @param context The context
* @return The intent builder
*/
public static PhotoViewIntentBuilder newPhotoViewFragmentIntentBuilder(Context context) {
return newPhotoViewFragmentIntentBuilder(context, PhotoViewFragment.class);
}
/**
* Gets a photo view intent builder to display the photo view fragment with a custom fragment
* subclass.
*
* @param context The context
* @param clazz Subclass of PhotoViewFragment to use
* @return The intent builder
*/
public static PhotoViewIntentBuilder newPhotoViewFragmentIntentBuilder(Context context,
Class<? extends PhotoViewFragment> clazz) {
return new PhotoViewIntentBuilder(context, clazz);
}
/** Gets a new photo view intent builder */
public static PhotoViewIntentBuilder newPhotoViewIntentBuilder(
Context context, Class<? extends Activity> cls) {
return new PhotoViewIntentBuilder(context, cls);
}
/** Gets a new photo view intent builder */
public static PhotoViewIntentBuilder newPhotoViewIntentBuilder(
Context context, String activityName) {
return new PhotoViewIntentBuilder(context, activityName);
}
/** Builder to create a photo view intent */
public static class PhotoViewIntentBuilder {
private final Intent mIntent;
/** The index of the photo to show */
private Integer mPhotoIndex;
/** The URI of the initial photo to show */
private String mInitialPhotoUri;
/** The URI of the initial thumbnail to show */
private String mInitialThumbnailUri;
/** The URI of the group of photos to display */
private String mPhotosUri;
/** The URL of the photo to display */
private String mResolvedPhotoUri;
/** The projection for the query to use; optional */
private String[] mProjection;
/** The URI of a thumbnail of the photo to display */
private String mThumbnailUri;
/** The content Description for the photo to show */
private String mContentDescription;
/** The maximum scale to display images at before */
private Float mMaxInitialScale;
/** True if lights out should automatically be invoked on a timer basis */
private boolean mEnableTimerLightsOut;
/**
* True if the PhotoViewFragments should watch for network changes to restart their loaders
*/
private boolean mWatchNetwork;
/** true we want to run the image scale animation */
private boolean mScaleAnimation;
/** The parameters for performing the scale up/scale down animations
* upon enter and exit. StartX and StartY represent the screen coordinates
* of the upper left corner of the start rectangle, startWidth and startHeight
* represent the width and height of the start rectangle.
*/
private int mStartX;
private int mStartY;
private int mStartWidth;
private int mStartHeight;
private boolean mActionBarHiddenInitially;
private boolean mDisplayFullScreenThumbs;
private PhotoViewIntentBuilder(Context context, Class<?> cls) {
mIntent = new Intent(context, cls);
initialize();
}
private PhotoViewIntentBuilder(Context context, String activityName) {
mIntent = new Intent();
mIntent.setClassName(context, activityName);
initialize();
}
private void initialize() {
mScaleAnimation = false;
mActionBarHiddenInitially = false;
mDisplayFullScreenThumbs = false;
mEnableTimerLightsOut = true;
}
/** Sets auto lights out */
public PhotoViewIntentBuilder setEnableTimerLightsOut(boolean enable) {
mEnableTimerLightsOut = enable;
return this;
}
/** Sets the photo index */
public PhotoViewIntentBuilder setPhotoIndex(Integer photoIndex) {
mPhotoIndex = photoIndex;
return this;
}
/** Sets the initial photo URI */
public PhotoViewIntentBuilder setInitialPhotoUri(String initialPhotoUri) {
mInitialPhotoUri = initialPhotoUri;
return this;
}
/** Sets the photos URI */
public PhotoViewIntentBuilder setPhotosUri(String photosUri) {
mPhotosUri = photosUri;
return this;
}
/** Sets the query projection */
public PhotoViewIntentBuilder setProjection(String[] projection) {
mProjection = projection;
return this;
}
/** Sets the resolved photo URI. This method is for the case
* where the URI given to {@link PhotoViewActivity} points directly
* to a single image and does not need to be resolved via a query
* to the {@link ContentProvider}. If this value is set, it supersedes
* {@link #setPhotosUri(String)}. */
public PhotoViewIntentBuilder setResolvedPhotoUri(String resolvedPhotoUri) {
mResolvedPhotoUri = resolvedPhotoUri;
return this;
}
/**
* Sets the URI for a thumbnail preview of the photo.
*/
public PhotoViewIntentBuilder setThumbnailUri(String thumbnailUri) {
mThumbnailUri = thumbnailUri;
return this;
}
/**
* Sets the content Description for the photo
*/
public PhotoViewIntentBuilder setContentDescription(String contentDescription) {
mContentDescription = contentDescription;
return this;
}
/**
* Sets the maximum scale which an image is initially displayed at
*/
public PhotoViewIntentBuilder setMaxInitialScale(float maxScale) {
mMaxInitialScale = maxScale;
return this;
}
/**
* Enable watching the network for connectivity changes.
*
* When a change is detected, bitmap loaders will be restarted if required.
*/
public PhotoViewIntentBuilder watchNetworkConnectivityChanges() {
mWatchNetwork = true;
return this;
}
/**
* Enable a scale animation that animates the initial photo URI passed in using
* {@link #setInitialPhotoUri}.
*
* Note: To avoid janky transitions, particularly when exiting the photoviewer, ensure the
* following system UI flags are set on the root view of the relying app's activity
* (via @{link View.setSystemUiVisibility(int)}):
* {@code View.SYSTEM_UI_FLAG_VISIBLE | View.SYSTEM_UI_FLAG_LAYOUT_STABLE}
* As well, client should ensure {@code android:fitsSystemWindows} is set on the root
* content view.
*/
public PhotoViewIntentBuilder setScaleAnimation(int startX, int startY,
int startWidth, int startHeight) {
mScaleAnimation = true;
mStartX = startX;
mStartY = startY;
mStartWidth = startWidth;
mStartHeight = startHeight;
return this;
}
// If this option is turned on, then the photoViewer will be initially
// displayed with the action bar hidden. This is as opposed to the default
// behavior, where the actionBar is initially shown.
public PhotoViewIntentBuilder setActionBarHiddenInitially(
boolean actionBarHiddenInitially) {
mActionBarHiddenInitially = actionBarHiddenInitially;
return this;
}
// If this option is turned on, then the small, lo-res thumbnail will
// be scaled up to the maximum size to cover as much of the screen as
// possible while still maintaining the correct aspect ratio. This means
// that the image may appear blurry until the the full-res image is
// loaded.
// This is as opposed to the default behavior, where only part of the
// thumbnail is displayed in a small view in the center of the screen,
// and a loading spinner is displayed until the full-res image is loaded.
public PhotoViewIntentBuilder setDisplayThumbsFullScreen(
boolean displayFullScreenThumbs) {
mDisplayFullScreenThumbs = displayFullScreenThumbs;
return this;
}
/** Build the intent */
public Intent build() {
mIntent.setAction(Intent.ACTION_VIEW);
// In Lollipop, each list of photos should appear as a document in the "Recents"
// list. In earlier versions, this flag was Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET.
mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT
// FLAG_ACTIVITY_CLEAR_TOP is needed for the case where the app tries to
// display a different photo while there is an existing activity instance
// for that list of photos. Since the initial photo is specified as an
// extra, without FLAG_ACTIVITY_CLEAR_TOP, the activity instance would
// just get restarted and it would display whatever photo it was last
// displaying. FLAG_ACTIVITY_CLEAR_TOP causes a new instance to be created,
// and it will display the new initial photo.
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
if (mPhotoIndex != null) {
mIntent.putExtra(EXTRA_PHOTO_INDEX, (int) mPhotoIndex);
}
if (mInitialPhotoUri != null) {
mIntent.putExtra(EXTRA_INITIAL_PHOTO_URI, mInitialPhotoUri);
}
if (mInitialPhotoUri != null && mPhotoIndex != null) {
throw new IllegalStateException(
"specified both photo index and photo uri");
}
if (mPhotosUri != null) {
mIntent.putExtra(EXTRA_PHOTOS_URI, mPhotosUri);
mIntent.setData(Uri.parse(mPhotosUri));
}
if (mResolvedPhotoUri != null) {
mIntent.putExtra(EXTRA_RESOLVED_PHOTO_URI, mResolvedPhotoUri);
}
if (mProjection != null) {
mIntent.putExtra(EXTRA_PROJECTION, mProjection);
}
if (mThumbnailUri != null) {
mIntent.putExtra(EXTRA_THUMBNAIL_URI, mThumbnailUri);
}
if (mContentDescription != null) {
mIntent.putExtra(EXTRA_CONTENT_DESCRIPTION, mContentDescription);
}
if (mMaxInitialScale != null) {
mIntent.putExtra(EXTRA_MAX_INITIAL_SCALE, mMaxInitialScale);
}
mIntent.putExtra(EXTRA_WATCH_NETWORK, mWatchNetwork);
mIntent.putExtra(EXTRA_SCALE_UP_ANIMATION, mScaleAnimation);
if (mScaleAnimation) {
mIntent.putExtra(EXTRA_ANIMATION_START_X, mStartX);
mIntent.putExtra(EXTRA_ANIMATION_START_Y, mStartY);
mIntent.putExtra(EXTRA_ANIMATION_START_WIDTH, mStartWidth);
mIntent.putExtra(EXTRA_ANIMATION_START_HEIGHT, mStartHeight);
}
mIntent.putExtra(EXTRA_ACTION_BAR_HIDDEN_INITIALLY, mActionBarHiddenInitially);
mIntent.putExtra(EXTRA_DISPLAY_THUMBS_FULLSCREEN, mDisplayFullScreenThumbs);
mIntent.putExtra(EXTRA_ENABLE_TIMER_LIGHTS_OUT, mEnableTimerLightsOut);
return mIntent;
}
}
}
@@ -0,0 +1,101 @@
package com.android.ex.photo;
import android.database.Cursor;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import androidx.loader.content.Loader;
import com.android.ex.photo.adapters.PhotoPagerAdapter;
import com.android.ex.photo.fragments.PhotoViewFragment;
import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult;
public interface PhotoViewCallbacks {
public static final int BITMAP_LOADER_AVATAR = 1;
public static final int BITMAP_LOADER_THUMBNAIL = 2;
public static final int BITMAP_LOADER_PHOTO = 3;
/**
* Listener to be invoked for screen events.
*/
public static interface OnScreenListener {
/**
* The full screen state has changed.
*/
public void onFullScreenChanged(boolean fullScreen);
/**
* A new view has been activated and the previous view de-activated.
*/
public void onViewActivated();
/**
* This view is a candidate for being the next view.
*
* This will be called when the view is focused completely on the view immediately before
* or after this one, so that this view can reset itself if nessecary.
*/
public void onViewUpNext();
/**
* Called when a right-to-left touch move intercept is about to occur.
*
* @param origX the raw x coordinate of the initial touch
* @param origY the raw y coordinate of the initial touch
* @return {@code true} if the touch should be intercepted.
*/
public boolean onInterceptMoveLeft(float origX, float origY);
/**
* Called when a left-to-right touch move intercept is about to occur.
*
* @param origX the raw x coordinate of the initial touch
* @param origY the raw y coordinate of the initial touch
* @return {@code true} if the touch should be intercepted.
*/
public boolean onInterceptMoveRight(float origX, float origY);
}
public static interface CursorChangedListener {
/**
* Called when the cursor that contains the photo list data
* is updated. Note that there is no guarantee that the cursor
* will be at the proper position.
* @param cursor the cursor containing the photo list data
*/
public void onCursorChanged(Cursor cursor);
}
public void addScreenListener(int position, OnScreenListener listener);
public void removeScreenListener(int position);
public void addCursorListener(CursorChangedListener listener);
public void removeCursorListener(CursorChangedListener listener);
public void setViewActivated(int position);
public void onNewPhotoLoaded(int position);
public void onFragmentPhotoLoadComplete(PhotoViewFragment fragment,
boolean success);
public void toggleFullScreen();
public boolean isFragmentActive(Fragment fragment);
public void onFragmentVisible(PhotoViewFragment fragment);
public boolean isFragmentFullScreen(Fragment fragment);
public void onCursorChanged(PhotoViewFragment fragment, Cursor cursor);
public Loader<BitmapResult> onCreateBitmapLoader(int id, Bundle args, String uri);
/**
* Returns the adapter associated with this activity.
*/
public PhotoPagerAdapter getAdapter();
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,189 @@
/*
* Copyright (C) 2011 Google Inc.
* Licensed to 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.android.ex.photo;
import android.content.Context;
import androidx.core.view.MotionEventCompat;
import androidx.viewpager.widget.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
/**
* View pager for photo view fragments. Define our own class so we can specify the
* view pager in XML.
*/
public class PhotoViewPager extends ViewPager {
/**
* A type of intercept that should be performed
*/
public static enum InterceptType { NONE, LEFT, RIGHT, BOTH }
/**
* Provides an ability to intercept touch events.
* <p>
* {@link ViewPager} intercepts all touch events and we need to be able to override this
* behavior. Instead, we could perform a similar function by declaring a custom
* {@link android.view.ViewGroup} to contain the pager and intercept touch events at a higher
* level.
*/
public static interface OnInterceptTouchListener {
/**
* Called when a touch intercept is about to occur.
*
* @param origX the raw x coordinate of the initial touch
* @param origY the raw y coordinate of the initial touch
* @return Which type of touch, if any, should should be intercepted.
*/
public InterceptType onTouchIntercept(float origX, float origY);
}
private static final int INVALID_POINTER = -1;
private float mLastMotionX;
private int mActivePointerId;
/** The x coordinate where the touch originated */
private float mActivatedX;
/** The y coordinate where the touch originated */
private float mActivatedY;
private OnInterceptTouchListener mListener;
public PhotoViewPager(Context context) {
super(context);
initialize();
}
public PhotoViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
private void initialize() {
// Set the page transformer to perform the transition animation
// for each page in the view.
setPageTransformer(true, new PageTransformer() {
@Override
public void transformPage(View page, float position) {
// The >= 1 is needed so that the page
// (page A) that transitions behind the newly visible
// page (page B) that comes in from the left does not
// get the touch events because it is still on screen
// (page A is still technically on screen despite being
// invisible). This makes sure that when the transition
// has completely finished, we revert it to its default
// behavior and move it off of the screen.
if (position < 0 || position >= 1.f) {
page.setTranslationX(0);
page.setAlpha(1.f);
page.setScaleX(1);
page.setScaleY(1);
} else {
page.setTranslationX(-position * page.getWidth());
page.setAlpha(Math.max(0,1.f - position));
final float scale = Math.max(0, 1.f - position * 0.3f);
page.setScaleX(scale);
page.setScaleY(scale);
}
}
});
}
/**
* {@inheritDoc}
* <p>
* We intercept touch event intercepts so we can prevent switching views when the
* current view is internally scrollable.
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final InterceptType intercept = (mListener != null)
? mListener.onTouchIntercept(mActivatedX, mActivatedY)
: InterceptType.NONE;
final boolean ignoreScrollLeft =
(intercept == InterceptType.BOTH || intercept == InterceptType.LEFT);
final boolean ignoreScrollRight =
(intercept == InterceptType.BOTH || intercept == InterceptType.RIGHT);
// Only check ability to page if we can't scroll in one / both directions
final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mActivePointerId = INVALID_POINTER;
}
switch (action) {
case MotionEvent.ACTION_MOVE: {
if (ignoreScrollLeft || ignoreScrollRight) {
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
break;
}
final int pointerIndex =
MotionEventCompat.findPointerIndex(ev, activePointerId);
final float x = MotionEventCompat.getX(ev, pointerIndex);
if (ignoreScrollLeft && ignoreScrollRight) {
mLastMotionX = x;
return false;
} else if (ignoreScrollLeft && (x > mLastMotionX)) {
mLastMotionX = x;
return false;
} else if (ignoreScrollRight && (x < mLastMotionX)) {
mLastMotionX = x;
return false;
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
mLastMotionX = ev.getX();
// Use the raw x/y as the children can be located anywhere and there isn't a
// single offset that would be meaningful
mActivatedX = ev.getRawX();
mActivatedY = ev.getRawY();
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
break;
}
case MotionEventCompat.ACTION_POINTER_UP: {
final int pointerIndex = MotionEventCompat.getActionIndex(ev);
final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
if (pointerId == mActivePointerId) {
// Our active pointer going up; select a new active pointer
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex);
mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
}
break;
}
}
return super.onInterceptTouchEvent(ev);
}
/**
* sets the intercept touch listener.
*/
public void setOnInterceptTouchListener(OnInterceptTouchListener l) {
mListener = l;
}
}
@@ -0,0 +1,259 @@
/*
* Copyright (C) 2011 Google Inc.
* Licensed to 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.android.ex.photo.adapters;
import android.content.Context;
import android.database.Cursor;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.View;
import com.android.ex.photo.provider.PhotoContract;
import java.util.HashMap;
/**
* Page adapter for use with an BaseCursorLoader. Unlike other cursor adapters, this has no
* observers for automatic refresh. Instead, it depends upon external mechanisms to provide
* the update signal.
*/
public abstract class BaseCursorPagerAdapter extends BaseFragmentPagerAdapter {
private static final String TAG = "BaseCursorPagerAdapter";
protected Context mContext;
protected Cursor mCursor;
protected int mRowIDColumn;
/** Mapping of row ID to cursor position */
protected SparseIntArray mItemPosition;
/** Mapping of instantiated object to row ID */
protected final HashMap<Object, Integer> mObjectRowMap = new HashMap<Object, Integer>();
/**
* Constructor that always enables auto-requery.
*
* @param c The cursor from which to get the data.
* @param context The context
*/
public BaseCursorPagerAdapter(Context context, FragmentManager fm, Cursor c) {
super(fm);
init(context, c);
}
/**
* Makes a fragment for the data pointed to by the cursor
*
* @param context Interface to application's global information
* @param cursor The cursor from which to get the data. The cursor is already
* moved to the correct position.
* @return the newly created fragment.
*/
public abstract Fragment getItem(Context context, Cursor cursor, int position);
// TODO: This shouldn't just return null - maybe it needs to wait for a cursor to be supplied?
// See b/7103023
@Override
public Fragment getItem(int position) {
if (mCursor != null && moveCursorTo(position)) {
return getItem(mContext, mCursor, position);
}
return null;
}
@Override
public int getCount() {
if (mCursor != null) {
return mCursor.getCount();
} else {
return 0;
}
}
@Override
public Object instantiateItem(View container, int position) {
if (mCursor == null) {
throw new IllegalStateException("this should only be called when the cursor is valid");
}
final Integer rowId;
if (moveCursorTo(position)) {
rowId = mCursor.getString(mRowIDColumn).hashCode();
} else {
rowId = null;
}
// Create the fragment and bind cursor data
final Object obj = super.instantiateItem(container, position);
if (obj != null) {
mObjectRowMap.put(obj, rowId);
}
return obj;
}
@Override
public void destroyItem(View container, int position, Object object) {
mObjectRowMap.remove(object);
super.destroyItem(container, position, object);
}
@Override
public int getItemPosition(Object object) {
final Integer rowId = mObjectRowMap.get(object);
if (rowId == null || mItemPosition == null) {
return POSITION_NONE;
}
final int position = mItemPosition.get(rowId, POSITION_NONE);
return position;
}
/**
* @return true if data is valid
*/
public boolean isDataValid() {
return mCursor != null;
}
/**
* Returns the cursor.
*/
public Cursor getCursor() {
return mCursor;
}
/**
* Returns the data item associated with the specified position in the data set.
*/
public Object getDataItem(int position) {
if (mCursor != null && moveCursorTo(position)) {
return mCursor;
} else {
return null;
}
}
/**
* Returns the row id associated with the specified position in the list.
*/
public long getItemId(int position) {
if (mCursor != null && moveCursorTo(position)) {
return mCursor.getString(mRowIDColumn).hashCode();
} else {
return 0;
}
}
/**
* Swap in a new Cursor, returning the old Cursor.
*
* @param newCursor The new cursor to be used.
* @return Returns the previously set Cursor, or null if there was not one.
* If the given new Cursor is the same instance is the previously set
* Cursor, null is also returned.
*/
public Cursor swapCursor(Cursor newCursor) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "swapCursor old=" + (mCursor == null ? -1 : mCursor.getCount()) +
"; new=" + (newCursor == null ? -1 : newCursor.getCount()));
}
if (newCursor == mCursor) {
return null;
}
Cursor oldCursor = mCursor;
mCursor = newCursor;
if (newCursor != null) {
mRowIDColumn = newCursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI);
} else {
mRowIDColumn = -1;
}
setItemPosition();
notifyDataSetChanged(); // notify the observers about the new cursor
return oldCursor;
}
/**
* Converts the cursor into a CharSequence. Subclasses should override this
* method to convert their results. The default implementation returns an
* empty String for null values or the default String representation of
* the value.
*
* @param cursor the cursor to convert to a CharSequence
* @return a CharSequence representing the value
*/
public CharSequence convertToString(Cursor cursor) {
return cursor == null ? "" : cursor.toString();
}
@Override
protected String makeFragmentName(int viewId, int index) {
if (moveCursorTo(index)) {
return "android:pager:" + viewId + ":" + mCursor.getString(mRowIDColumn).hashCode();
} else {
return super.makeFragmentName(viewId, index);
}
}
/**
* Moves the cursor to the given position
*
* @return {@code true} if the cursor's position was set. Otherwise, {@code false}.
*/
private boolean moveCursorTo(int position) {
if (mCursor != null && !mCursor.isClosed()) {
return mCursor.moveToPosition(position);
}
return false;
}
/**
* Initialize the adapter.
*/
private void init(Context context, Cursor c) {
boolean cursorPresent = c != null;
mCursor = c;
mContext = context;
mRowIDColumn = cursorPresent
? mCursor.getColumnIndex(PhotoContract.PhotoViewColumns.URI) : -1;
}
/**
* Sets the {@link #mItemPosition} instance variable with the current mapping of
* row id to cursor position.
*/
private void setItemPosition() {
if (mCursor == null || mCursor.isClosed()) {
mItemPosition = null;
return;
}
SparseIntArray itemPosition = new SparseIntArray(mCursor.getCount());
mCursor.moveToPosition(-1);
while (mCursor.moveToNext()) {
final int rowId = mCursor.getString(mRowIDColumn).hashCode();
final int position = mCursor.getPosition();
itemPosition.append(rowId, position);
}
mItemPosition = itemPosition;
}
}
@@ -0,0 +1,191 @@
/*
* Copyright (C) 2011 Google Inc.
* Licensed to 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.android.ex.photo.adapters;
import android.os.Parcelable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.collection.LruCache;
import androidx.viewpager.widget.PagerAdapter;
import android.util.Log;
import android.view.View;
/**
* NOTE: This is a direct copy of {@link androidx.fragment.app.FragmentPagerAdapter}
* with four very important modifications.
* <p>
* <ol>
* <li>The method {@link #makeFragmentName(int, int)} is declared "protected"
* in our class. We need to be able to re-define the fragment's name according to data
* only available to sub-classes.</li>
* <li>The method {@link #isViewFromObject(View, Object)} has been reimplemented to search
* the entire view hierarchy for the given view.</li>
* <li>In method {@link #destroyItem(View, int, Object)}, the fragment is detached and
* added to a cache. If the fragment is evicted from the cache, it will be deleted.
* An album may contain thousands of photos and we want to avoid having thousands of
* fragments.</li>
* </ol>
*/
public abstract class BaseFragmentPagerAdapter extends PagerAdapter {
/** The default size of {@link #mFragmentCache} */
private static final int DEFAULT_CACHE_SIZE = 5;
private static final String TAG = "FragmentPagerAdapter";
private static final boolean DEBUG = false;
private final FragmentManager mFragmentManager;
private FragmentTransaction mCurTransaction = null;
private Fragment mCurrentPrimaryItem = null;
/** A cache to store detached fragments before they are removed */
private LruCache<String, Fragment> mFragmentCache = new FragmentCache(DEFAULT_CACHE_SIZE);
public BaseFragmentPagerAdapter(androidx.fragment.app.FragmentManager fm) {
mFragmentManager = fm;
}
/**
* Return the Fragment associated with a specified position.
*/
public abstract Fragment getItem(int position);
@Override
public void startUpdate(View container) {
}
@Override
public Object instantiateItem(View container, int position) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
// Do we already have this fragment?
String name = makeFragmentName(container.getId(), position);
// Remove item from the cache
mFragmentCache.remove(name);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + position + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
if(fragment == null) {
if (DEBUG) Log.e(TAG, "NPE workaround for getItem(). See b/7103023");
return null;
}
if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), position));
}
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
}
return fragment;
}
@Override
public void destroyItem(View container, int position, Object object) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Detaching item #" + position + ": f=" + object
+ " v=" + ((Fragment)object).getView());
Fragment fragment = (Fragment) object;
String name = fragment.getTag();
if (name == null) {
// We prefer to get the name directly from the fragment, but, if the fragment is
// detached before the add transaction is committed, this could be 'null'. In
// that case, generate a name so we can still cache the fragment.
name = makeFragmentName(container.getId(), position);
}
mFragmentCache.put(name, fragment);
mCurTransaction.detach(fragment);
}
@Override
public void setPrimaryItem(View container, int position, Object object) {
Fragment fragment = (Fragment) object;
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
mCurrentPrimaryItem.setMenuVisibility(false);
}
if (fragment != null) {
fragment.setMenuVisibility(true);
}
mCurrentPrimaryItem = fragment;
}
}
@Override
public void finishUpdate(View container) {
if (mCurTransaction != null && !mFragmentManager.isDestroyed()) {
mCurTransaction.commitAllowingStateLoss();
mCurTransaction = null;
mFragmentManager.executePendingTransactions();
}
}
@Override
public boolean isViewFromObject(View view, Object object) {
// Ascend the tree to determine if the view is a child of the fragment
View root = ((Fragment) object).getView();
for (Object v = view; v instanceof View; v = ((View) v).getParent()) {
if (v == root) {
return true;
}
}
return false;
}
@Override
public Parcelable saveState() {
return null;
}
@Override
public void restoreState(Parcelable state, ClassLoader loader) {
}
/** Creates a name for the fragment */
protected String makeFragmentName(int viewId, int index) {
return "android:switcher:" + viewId + ":" + index;
}
/**
* A cache of detached fragments.
*/
private class FragmentCache extends LruCache<String, Fragment> {
public FragmentCache(int size) {
super(size);
}
@Override
protected void entryRemoved(boolean evicted, String key,
Fragment oldValue, Fragment newValue) {
// remove the fragment if it's evicted OR it's replaced by a new fragment
if (evicted || (newValue != null && oldValue != newValue)) {
mCurTransaction.remove(oldValue);
}
}
}
}
@@ -0,0 +1,134 @@
/*
* Copyright (C) 2011 Google Inc.
* Licensed to 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.android.ex.photo.adapters;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import androidx.fragment.app.Fragment;
import androidx.collection.SimpleArrayMap;
import com.android.ex.photo.Intents;
import com.android.ex.photo.Intents.PhotoViewIntentBuilder;
import com.android.ex.photo.fragments.PhotoViewFragment;
import com.android.ex.photo.provider.PhotoContract.PhotoViewColumns;
import com.android.ex.photo.provider.PhotoContract.PhotoQuery;
/**
* Pager adapter for the photo view
*/
public class PhotoPagerAdapter extends BaseCursorPagerAdapter {
protected SimpleArrayMap<String, Integer> mColumnIndices =
new SimpleArrayMap<String, Integer>(PhotoQuery.PROJECTION.length);
protected final float mMaxScale;
protected boolean mDisplayThumbsFullScreen;
public PhotoPagerAdapter(
Context context, androidx.fragment.app.FragmentManager fm, Cursor c,
float maxScale, boolean thumbsFullScreen) {
super(context, fm, c);
mMaxScale = maxScale;
mDisplayThumbsFullScreen = thumbsFullScreen;
}
@Override
public Fragment getItem(Context context, Cursor cursor, int position) {
final String photoUri = getPhotoUri(cursor);
final String thumbnailUri = getThumbnailUri(cursor);
final String contentDescription = getPhotoName(cursor);
boolean loading = shouldShowLoadingIndicator(cursor);
boolean onlyShowSpinner = false;
if(photoUri == null && loading) {
onlyShowSpinner = true;
}
// create new PhotoViewFragment
final PhotoViewIntentBuilder builder =
Intents.newPhotoViewFragmentIntentBuilder(mContext, getPhotoViewFragmentClass());
builder
.setResolvedPhotoUri(photoUri)
.setThumbnailUri(thumbnailUri)
.setContentDescription(contentDescription)
.setDisplayThumbsFullScreen(mDisplayThumbsFullScreen)
.setMaxInitialScale(mMaxScale);
return createPhotoViewFragment(builder.build(), position, onlyShowSpinner);
}
protected Class<? extends PhotoViewFragment> getPhotoViewFragmentClass() {
return PhotoViewFragment.class;
}
protected PhotoViewFragment createPhotoViewFragment(
Intent intent, int position, boolean onlyShowSpinner) {
return PhotoViewFragment.newInstance(intent, position, onlyShowSpinner);
}
@Override
public Cursor swapCursor(Cursor newCursor) {
mColumnIndices.clear();
if (newCursor != null) {
for(String column : PhotoQuery.PROJECTION) {
mColumnIndices.put(column, newCursor.getColumnIndexOrThrow(column));
}
for(String column : PhotoQuery.OPTIONAL_COLUMNS) {
int index = newCursor.getColumnIndex(column);
if (index != -1) {
mColumnIndices.put(column, index);
}
}
}
return super.swapCursor(newCursor);
}
public String getPhotoUri(Cursor cursor) {
return getString(cursor, PhotoViewColumns.CONTENT_URI);
}
public String getThumbnailUri(Cursor cursor) {
return getString(cursor, PhotoViewColumns.THUMBNAIL_URI);
}
public String getContentType(Cursor cursor) {
return getString(cursor, PhotoViewColumns.CONTENT_TYPE);
}
public String getPhotoName(Cursor cursor) {
return getString(cursor, PhotoViewColumns.NAME);
}
public boolean shouldShowLoadingIndicator(Cursor cursor) {
String value = getString(cursor, PhotoViewColumns.LOADING_INDICATOR);
if (value == null) {
return false;
} else {
return Boolean.parseBoolean(value);
}
}
private String getString(Cursor cursor, String column) {
if (mColumnIndices.containsKey(column)) {
return cursor.getString(mColumnIndices.get(column));
} else {
return null;
}
}
}
@@ -0,0 +1,606 @@
/*
* Copyright (C) 2011 Google Inc.
* Licensed to 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.android.ex.photo.fragments;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.android.ex.photo.Intents;
import com.android.ex.photo.PhotoViewCallbacks;
import com.android.ex.photo.PhotoViewCallbacks.CursorChangedListener;
import com.android.ex.photo.PhotoViewCallbacks.OnScreenListener;
import com.android.ex.photo.PhotoViewController.ActivityInterface;
import com.android.ex.photo.R;
import com.android.ex.photo.adapters.PhotoPagerAdapter;
import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface;
import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult;
import com.android.ex.photo.views.PhotoView;
import com.android.ex.photo.views.ProgressBarWrapper;
/**
* Displays a photo.
*/
public class PhotoViewFragment extends Fragment implements
LoaderManager.LoaderCallbacks<BitmapResult>,
OnClickListener,
OnScreenListener,
CursorChangedListener {
/**
* Interface for components that are internally scrollable left-to-right.
*/
public static interface HorizontallyScrollable {
/**
* Return {@code true} if the component needs to receive right-to-left
* touch movements.
*
* @param origX the raw x coordinate of the initial touch
* @param origY the raw y coordinate of the initial touch
*/
public boolean interceptMoveLeft(float origX, float origY);
/**
* Return {@code true} if the component needs to receive left-to-right
* touch movements.
*
* @param origX the raw x coordinate of the initial touch
* @param origY the raw y coordinate of the initial touch
*/
public boolean interceptMoveRight(float origX, float origY);
}
protected final static String STATE_INTENT_KEY =
"com.android.mail.photo.fragments.PhotoViewFragment.INTENT";
protected final static String ARG_INTENT = "arg-intent";
protected final static String ARG_POSITION = "arg-position";
protected final static String ARG_SHOW_SPINNER = "arg-show-spinner";
/** The URL of a photo to display */
protected String mResolvedPhotoUri;
protected String mThumbnailUri;
protected String mContentDescription;
/** The intent we were launched with */
protected Intent mIntent;
protected PhotoViewCallbacks mCallback;
protected PhotoPagerAdapter mAdapter;
protected BroadcastReceiver mInternetStateReceiver;
protected PhotoView mPhotoView;
protected ImageView mPhotoPreviewImage;
protected TextView mEmptyText;
protected ImageView mRetryButton;
protected ProgressBarWrapper mPhotoProgressBar;
protected int mPosition;
/** Whether or not the fragment should make the photo full-screen */
protected boolean mFullScreen;
/**
* True if the PhotoViewFragment should watch the network state in order to restart loaders.
*/
protected boolean mWatchNetworkState;
/** Whether or not this fragment will only show the loading spinner */
protected boolean mOnlyShowSpinner;
/** Whether or not the progress bar is showing valid information about the progress stated */
protected boolean mProgressBarNeeded = true;
protected View mPhotoPreviewAndProgress;
protected boolean mThumbnailShown;
/** Whether or not there is currently a connection to the internet */
protected boolean mConnected;
/** Whether or not we can display the thumbnail at fullscreen size */
protected boolean mDisplayThumbsFullScreen;
/** Public no-arg constructor for allowing the framework to handle orientation changes */
public PhotoViewFragment() {
// Do nothing.
}
/**
* Create a {@link PhotoViewFragment}.
* @param intent
* @param position
* @param onlyShowSpinner
*/
public static PhotoViewFragment newInstance(
Intent intent, int position, boolean onlyShowSpinner) {
final PhotoViewFragment f = new PhotoViewFragment();
initializeArguments(intent, position, onlyShowSpinner, f);
return f;
}
public static void initializeArguments(
Intent intent, int position, boolean onlyShowSpinner, PhotoViewFragment f) {
final Bundle b = new Bundle();
b.putParcelable(ARG_INTENT, intent);
b.putInt(ARG_POSITION, position);
b.putBoolean(ARG_SHOW_SPINNER, onlyShowSpinner);
f.setArguments(b);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mCallback = getCallbacks();
if (mCallback == null) {
throw new IllegalArgumentException(
"Activity must be a derived class of PhotoViewActivity");
}
mAdapter = mCallback.getAdapter();
if (mAdapter == null) {
throw new IllegalStateException("Callback reported null adapter");
}
// Don't call until we've setup the entire view
setViewVisibility();
}
protected PhotoViewCallbacks getCallbacks() {
return ((ActivityInterface) getActivity()).getController();
}
@Override
public void onDetach() {
mCallback = null;
super.onDetach();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Bundle bundle = getArguments();
if (bundle == null) {
return;
}
mIntent = bundle.getParcelable(ARG_INTENT);
mDisplayThumbsFullScreen = mIntent.getBooleanExtra(
Intents.EXTRA_DISPLAY_THUMBS_FULLSCREEN, false);
mPosition = bundle.getInt(ARG_POSITION);
mOnlyShowSpinner = bundle.getBoolean(ARG_SHOW_SPINNER);
mProgressBarNeeded = true;
if (savedInstanceState != null) {
final Bundle state = savedInstanceState.getBundle(STATE_INTENT_KEY);
if (state != null) {
mIntent = new Intent().putExtras(state);
}
}
if (mIntent != null) {
mResolvedPhotoUri = mIntent.getStringExtra(Intents.EXTRA_RESOLVED_PHOTO_URI);
mThumbnailUri = mIntent.getStringExtra(Intents.EXTRA_THUMBNAIL_URI);
mContentDescription = mIntent.getStringExtra(Intents.EXTRA_CONTENT_DESCRIPTION);
mWatchNetworkState = mIntent.getBooleanExtra(Intents.EXTRA_WATCH_NETWORK, false);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.photo_fragment_view, container, false);
initializeView(view);
return view;
}
protected void initializeView(View view) {
mPhotoView = (PhotoView) view.findViewById(R.id.photo_view);
mPhotoView.setMaxInitialScale(mIntent.getFloatExtra(Intents.EXTRA_MAX_INITIAL_SCALE, 1));
mPhotoView.setOnClickListener(this);
mPhotoView.setFullScreen(mFullScreen, false);
mPhotoView.enableImageTransforms(false);
mPhotoView.setContentDescription(mContentDescription);
mPhotoPreviewAndProgress = view.findViewById(R.id.photo_preview);
mPhotoPreviewImage = (ImageView) view.findViewById(R.id.photo_preview_image);
mThumbnailShown = false;
final ProgressBar indeterminate =
(ProgressBar) view.findViewById(R.id.indeterminate_progress);
final ProgressBar determinate =
(ProgressBar) view.findViewById(R.id.determinate_progress);
mPhotoProgressBar = new ProgressBarWrapper(determinate, indeterminate, true);
mEmptyText = (TextView) view.findViewById(R.id.empty_text);
mRetryButton = (ImageView) view.findViewById(R.id.retry_button);
// Don't call until we've setup the entire view
setViewVisibility();
}
@Override
public void onResume() {
super.onResume();
mCallback.addScreenListener(mPosition, this);
mCallback.addCursorListener(this);
if (mWatchNetworkState) {
if (mInternetStateReceiver == null) {
mInternetStateReceiver = new InternetStateBroadcastReceiver();
}
getActivity().registerReceiver(mInternetStateReceiver,
new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
ConnectivityManager connectivityManager = (ConnectivityManager)
getActivity().getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo();
if (activeNetInfo != null) {
mConnected = activeNetInfo.isConnected();
} else {
// Best to set this to false, since it won't stop us from trying to download,
// only allow us to try re-download if we get notified that we do have a connection.
mConnected = false;
}
}
if (!isPhotoBound()) {
mProgressBarNeeded = true;
mPhotoPreviewAndProgress.setVisibility(View.VISIBLE);
getLoaderManager().initLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL,
null, this);
// FLAG: If we are displaying thumbnails at fullscreen size, then we
// could defer the loading of the fullscreen image until the thumbnail
// has finished loading, or even until the user attempts to zoom in.
getLoaderManager().initLoader(PhotoViewCallbacks.BITMAP_LOADER_PHOTO,
null, this);
}
}
@Override
public void onPause() {
// Remove listeners
if (mWatchNetworkState) {
getActivity().unregisterReceiver(mInternetStateReceiver);
}
mCallback.removeCursorListener(this);
mCallback.removeScreenListener(mPosition);
super.onPause();
}
@Override
public void onDestroyView() {
// Clean up views and other components
if (mPhotoView != null) {
mPhotoView.clear();
mPhotoView = null;
}
super.onDestroyView();
}
public String getPhotoUri() {
return mResolvedPhotoUri;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mIntent != null) {
outState.putParcelable(STATE_INTENT_KEY, mIntent.getExtras());
}
}
@Override
public Loader<BitmapResult> onCreateLoader(int id, Bundle args) {
if(mOnlyShowSpinner) {
return null;
}
String uri = null;
switch (id) {
case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL:
uri = mThumbnailUri;
break;
case PhotoViewCallbacks.BITMAP_LOADER_PHOTO:
uri = mResolvedPhotoUri;
break;
}
return mCallback.onCreateBitmapLoader(id, args, uri);
}
@Override
public void onLoadFinished(Loader<BitmapResult> loader, BitmapResult result) {
// If we don't have a view, the fragment has been paused. We'll get the cursor again later.
// If we're not added, the fragment has detached during the loading process. We no longer
// need the result.
if (getView() == null || !isAdded()) {
return;
}
final Drawable data = result.getDrawable(getResources());
final int id = loader.getId();
switch (id) {
case PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL:
if (mDisplayThumbsFullScreen) {
displayPhoto(result);
} else {
if (isPhotoBound()) {
// There is need to do anything with the thumbnail
// image, as the full size image is being shown.
return;
}
if (data == null) {
// no preview, show default
mPhotoPreviewImage.setImageResource(R.drawable.default_image);
mThumbnailShown = false;
} else {
// show preview
mPhotoPreviewImage.setImageDrawable(data);
mThumbnailShown = true;
}
mPhotoPreviewImage.setVisibility(View.VISIBLE);
if (getResources().getBoolean(R.bool.force_thumbnail_no_scaling)) {
mPhotoPreviewImage.setScaleType(ImageView.ScaleType.CENTER);
}
enableImageTransforms(false);
}
break;
case PhotoViewCallbacks.BITMAP_LOADER_PHOTO:
displayPhoto(result);
break;
default:
break;
}
if (mProgressBarNeeded == false) {
// Hide the progress bar as it isn't needed anymore.
mPhotoProgressBar.setVisibility(View.GONE);
}
if (data != null) {
mCallback.onNewPhotoLoaded(mPosition);
}
setViewVisibility();
}
private void displayPhoto(BitmapResult result) {
if (result.status == BitmapResult.STATUS_EXCEPTION) {
mProgressBarNeeded = false;
mEmptyText.setText(R.string.failed);
mEmptyText.setVisibility(View.VISIBLE);
mCallback.onFragmentPhotoLoadComplete(this, false /* success */);
} else {
mEmptyText.setVisibility(View.GONE);
final Drawable data = result.getDrawable(getResources());
bindPhoto(data);
mCallback.onFragmentPhotoLoadComplete(this, true /* success */);
}
}
/**
* Binds an image to the photo view.
*/
private void bindPhoto(Drawable drawable) {
if (drawable != null) {
if (mPhotoView != null) {
mPhotoView.bindDrawable(drawable);
}
enableImageTransforms(true);
mPhotoPreviewAndProgress.setVisibility(View.GONE);
mProgressBarNeeded = false;
}
}
public Drawable getDrawable() {
return (mPhotoView != null ? mPhotoView.getDrawable() : null);
}
/**
* Enable or disable image transformations. When transformations are enabled, this view
* consumes all touch events.
*/
public void enableImageTransforms(boolean enable) {
mPhotoView.enableImageTransforms(enable);
}
@Override
public void onLoaderReset(Loader<BitmapResult> loader) {
// Do nothing
}
@Override
public void onClick(View v) {
mCallback.toggleFullScreen();
}
@Override
public void onFullScreenChanged(boolean fullScreen) {
setViewVisibility();
}
@Override
public void onViewUpNext() {
resetViews();
}
@Override
public void onViewActivated() {
if (!mCallback.isFragmentActive(this)) {
// we're not in the foreground; reset our view
resetViews();
} else {
if (!isPhotoBound()) {
// Restart the loader
getLoaderManager().restartLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL,
null, this);
}
mCallback.onFragmentVisible(this);
}
}
/**
* Reset the views to their default states
*/
public void resetViews() {
if (mPhotoView != null) {
mPhotoView.resetTransformations();
}
}
@Override
public boolean onInterceptMoveLeft(float origX, float origY) {
if (!mCallback.isFragmentActive(this)) {
// we're not in the foreground; don't intercept any touches
return false;
}
return (mPhotoView != null && mPhotoView.interceptMoveLeft(origX, origY));
}
@Override
public boolean onInterceptMoveRight(float origX, float origY) {
if (!mCallback.isFragmentActive(this)) {
// we're not in the foreground; don't intercept any touches
return false;
}
return (mPhotoView != null && mPhotoView.interceptMoveRight(origX, origY));
}
/**
* Returns {@code true} if a photo has been bound. Otherwise, returns {@code false}.
*/
public boolean isPhotoBound() {
return (mPhotoView != null && mPhotoView.isPhotoBound());
}
/**
* Sets view visibility depending upon whether or not we're in "full screen" mode.
*/
private void setViewVisibility() {
final boolean fullScreen = mCallback == null ? false : mCallback.isFragmentFullScreen(this);
setFullScreen(fullScreen);
}
/**
* Sets full-screen mode for the views.
*/
public void setFullScreen(boolean fullScreen) {
mFullScreen = fullScreen;
}
@Override
public void onCursorChanged(Cursor cursor) {
if (mAdapter == null) {
// The adapter is set in onAttach(), and is guaranteed to be non-null. We have magically
// received an onCursorChanged without attaching to an activity. Ignore this cursor
// change.
return;
}
// FLAG: There is a problem here:
// If the cursor changes, and new items are added at an earlier position than
// the current item, we will switch photos here. Really we should probably
// try to find a photo with the same url and move the cursor to that position.
if (cursor.moveToPosition(mPosition) && !isPhotoBound()) {
mCallback.onCursorChanged(this, cursor);
final LoaderManager manager = getLoaderManager();
final Loader<BitmapResult> fakePhotoLoader = manager.getLoader(
PhotoViewCallbacks.BITMAP_LOADER_PHOTO);
if (fakePhotoLoader != null) {
final PhotoBitmapLoaderInterface loader = (PhotoBitmapLoaderInterface) fakePhotoLoader;
mResolvedPhotoUri = mAdapter.getPhotoUri(cursor);
loader.setPhotoUri(mResolvedPhotoUri);
loader.forceLoad();
}
if (!mThumbnailShown) {
final Loader<BitmapResult> fakeThumbnailLoader = manager.getLoader(
PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL);
if (fakeThumbnailLoader != null) {
final PhotoBitmapLoaderInterface loader = (PhotoBitmapLoaderInterface) fakeThumbnailLoader;
mThumbnailUri = mAdapter.getThumbnailUri(cursor);
loader.setPhotoUri(mThumbnailUri);
loader.forceLoad();
}
}
}
}
public int getPosition() {
return mPosition;
}
public ProgressBarWrapper getPhotoProgressBar() {
return mPhotoProgressBar;
}
public TextView getEmptyText() {
return mEmptyText;
}
public ImageView getRetryButton() {
return mRetryButton;
}
public boolean isProgressBarNeeded() {
return mProgressBarNeeded;
}
private class InternetStateBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// This is only created if we have the correct permissions, so
ConnectivityManager connectivityManager = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo();
if (activeNetInfo == null || !activeNetInfo.isConnected()) {
mConnected = false;
return;
}
if (mConnected == false && !isPhotoBound()) {
if (mThumbnailShown == false) {
getLoaderManager().restartLoader(PhotoViewCallbacks.BITMAP_LOADER_THUMBNAIL,
null, PhotoViewFragment.this);
}
getLoaderManager().restartLoader(PhotoViewCallbacks.BITMAP_LOADER_PHOTO,
null, PhotoViewFragment.this);
mConnected = true;
mPhotoProgressBar.setVisibility(View.VISIBLE);
}
}
}
}
@@ -0,0 +1,173 @@
/*
* Copyright (C) 2011 Google Inc.
* Licensed to 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.android.ex.photo.loaders;
import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import androidx.loader.content.AsyncTaskLoader;
import android.util.DisplayMetrics;
import com.android.ex.photo.PhotoViewController;
import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult;
import com.android.ex.photo.util.ImageUtils;
/**
* Loader for the bitmap of a photo.
*/
public class PhotoBitmapLoader extends AsyncTaskLoader<BitmapResult>
implements PhotoBitmapLoaderInterface {
private String mPhotoUri;
private Bitmap mBitmap;
public PhotoBitmapLoader(Context context, String photoUri) {
super(context);
mPhotoUri = photoUri;
}
@Override
public void setPhotoUri(String photoUri) {
mPhotoUri = photoUri;
}
@Override
public BitmapResult loadInBackground() {
BitmapResult result = new BitmapResult();
Context context = getContext();
if (context != null && mPhotoUri != null) {
final ContentResolver resolver = context.getContentResolver();
try {
result = ImageUtils.createLocalBitmap(resolver, Uri.parse(mPhotoUri),
PhotoViewController.sMaxPhotoSize);
if (result.bitmap != null) {
result.bitmap.setDensity(DisplayMetrics.DENSITY_MEDIUM);
}
} catch (UnsupportedOperationException ex) {
// We got image bytes, but unable to decode to a Bitmap
result.status = BitmapResult.STATUS_EXCEPTION;
}
}
return result;
}
/**
* Called when there is new data to deliver to the client. The
* super class will take care of delivering it; the implementation
* here just adds a little more logic.
*/
@Override
public void deliverResult(BitmapResult result) {
Bitmap bitmap = result != null ? result.bitmap : null;
if (isReset()) {
// An async query came in while the loader is stopped. We
// don't need the result.
if (bitmap != null) {
onReleaseResources(bitmap);
}
return;
}
Bitmap oldBitmap = mBitmap;
mBitmap = bitmap;
if (isStarted()) {
// If the Loader is currently started, we can immediately
// deliver its results.
super.deliverResult(result);
}
// At this point we can release the resources associated with
// 'oldBitmap' if needed; now that the new result is delivered we
// know that it is no longer in use.
if (oldBitmap != null && oldBitmap != bitmap && !oldBitmap.isRecycled()) {
onReleaseResources(oldBitmap);
}
}
/**
* Handles a request to start the Loader.
*/
@Override
protected void onStartLoading() {
if (mBitmap != null) {
// If we currently have a result available, deliver it
// immediately.
BitmapResult result = new BitmapResult();
result.status = BitmapResult.STATUS_SUCCESS;
result.bitmap = mBitmap;
deliverResult(result);
}
if (takeContentChanged() || mBitmap == null) {
// If the data has changed since the last time it was loaded
// or is not currently available, start a load.
forceLoad();
}
}
/**
* Handles a request to stop the Loader.
*/
@Override protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
/**
* Handles a request to cancel a load.
*/
@Override
public void onCanceled(BitmapResult result) {
super.onCanceled(result);
// At this point we can release the resources associated with 'bitmap'
// if needed.
if (result != null) {
onReleaseResources(result.bitmap);
}
}
/**
* Handles a request to completely reset the Loader.
*/
@Override
protected void onReset() {
super.onReset();
// Ensure the loader is stopped
onStopLoading();
// At this point we can release the resources associated with 'bitmap'
// if needed.
if (mBitmap != null) {
onReleaseResources(mBitmap);
mBitmap = null;
}
}
/**
* Helper function to take care of releasing resources associated
* with an actively loaded data set.
*/
protected void onReleaseResources(Bitmap bitmap) {
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
}
}
@@ -0,0 +1,46 @@
package com.android.ex.photo.loaders;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
public interface PhotoBitmapLoaderInterface {
public void setPhotoUri(String photoUri);
public void forceLoad();
public static class BitmapResult {
public static final int STATUS_SUCCESS = 0;
public static final int STATUS_EXCEPTION = 1;
public Drawable drawable;
public Bitmap bitmap;
public int status;
/**
* Returns a drawable to be used in the {@link com.android.ex.photo.views.PhotoView}.
* Should return null if the drawable is not ready to be shown (for instance, if
* the underlying bitmap is null).
*/
public Drawable getDrawable(Resources resources) {
if (resources == null) {
throw new IllegalArgumentException("resources can not be null!");
}
if (drawable != null) {
return drawable;
}
// Don't create a new drawable if there's no bitmap. PhotoViewFragment regards
// a null drawable as a signal to keep showing the loading stuff.
// b/12348405.
if (bitmap == null) {
return null;
}
return new BitmapDrawable(resources, bitmap);
}
}
}
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2011 Google Inc.
* Licensed to 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.android.ex.photo.loaders;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import androidx.loader.content.CursorLoader;
import com.android.ex.photo.provider.PhotoContract;
/**
* Loader for a set of photo IDs.
*/
public class PhotoPagerLoader extends CursorLoader {
private final Uri mPhotosUri;
private final String[] mProjection;
public PhotoPagerLoader(
Context context, Uri photosUri, String[] projection) {
super(context);
mPhotosUri = photosUri;
mProjection = projection != null ? projection : PhotoContract.PhotoQuery.PROJECTION;
}
@Override
public Cursor loadInBackground() {
Cursor returnCursor = null;
final Uri loaderUri = mPhotosUri.buildUpon().appendQueryParameter(
PhotoContract.ContentTypeParameters.CONTENT_TYPE, "image/").build();
setUri(loaderUri);
setProjection(mProjection);
returnCursor = super.loadInBackground();
return returnCursor;
}
}
@@ -0,0 +1,82 @@
/*
* Copyright (C) 2011 Google Inc.
* Licensed to 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.android.ex.photo.provider;
import android.net.Uri;
import android.provider.OpenableColumns;
public final class PhotoContract {
/** Columns for the view */
public static interface PhotoViewColumns {
/**
* This column is a {@link Uri} that can be queried
* for this individual image (resulting cursor has one single row for this image).
*/
public static final String URI = "uri";
/**
* This column is a {@link String} that can be queried for this
* individual image to return a displayable name.
*/
public static final String NAME = OpenableColumns.DISPLAY_NAME;
/**
* This column is a {@link Uri} that points to the downloaded local file.
* Can be null.
*/
public static final String CONTENT_URI = "contentUri";
/**
* This column is a {@link Uri} that points to a thumbnail of the image
* that ideally is a local file.
* Can be null.
*/
public static final String THUMBNAIL_URI = "thumbnailUri";
/**
* This string column is the MIME type.
*/
public static final String CONTENT_TYPE = "contentType";
/**
* This boolean column indicates that a loading indicator should display permanently
* if no image urls are provided.
*/
public static final String LOADING_INDICATOR = "loadingIndicator";
}
public static interface PhotoQuery {
/** Projection of the returned cursor */
public final static String[] PROJECTION = {
PhotoViewColumns.URI,
PhotoViewColumns.NAME,
PhotoViewColumns.CONTENT_URI,
PhotoViewColumns.THUMBNAIL_URI,
PhotoViewColumns.CONTENT_TYPE
};
public final static String[] OPTIONAL_COLUMNS = {
PhotoViewColumns.LOADING_INDICATOR
};
}
public static final class ContentTypeParameters {
/**
* Parameter used to specify which type of content to return.
* Allows multiple types to be specified.
*/
public static final String CONTENT_TYPE = "contentType";
private ContentTypeParameters() {}
}
}
@@ -0,0 +1,211 @@
/*
* Copyright (C) 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.
*/
package com.android.ex.photo.util;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
public class Exif {
private static final String TAG = "CameraExif";
/**
* Returns the degrees in clockwise. Values are 0, 90, 180, or 270.
* @param inputStream The input stream will not be closed for you.
* @param byteSize Recommended parameter declaring the length of the input stream. If you
* pass in -1, we will have to read more from the input stream.
* @return 0, 90, 180, or 270.
*/
public static int getOrientation(final InputStream inputStream, final long byteSize) {
if (inputStream == null) {
return 0;
}
/*
Looking at this algorithm, we never look ahead more than 8 bytes. As long as we call
advanceTo() at the end of every loop, we should never have to reallocate a larger buffer.
Also, the most we ever read backwards is 4 bytes. pack() reads backwards if the encoding
is in little endian format. These following two lines potentially reads 4 bytes backwards:
int tag = pack(jpeg, offset, 4, false);
count = pack(jpeg, offset - 2, 2, littleEndian);
To be safe, we will always advance to some index-4, so we'll need 4 more for the +8
look ahead, which makes it a +12 look ahead total. Use 16 just in case my analysis is off.
This means we only need to allocate a single 16 byte buffer.
Note: If you do not pass in byteSize parameter, a single large allocation will occur.
For a 1MB image, I see one 30KB allocation. This is due to the line containing:
has(jpeg, byteSize, offset + length - 1)
where length is a variable int (around 30KB above) read from the EXIF headers.
This is still much better than allocating a 1MB byte[] which we were doing before.
*/
final int lookAhead = 16;
final int readBackwards = 4;
final InputStreamBuffer jpeg = new InputStreamBuffer(inputStream, lookAhead, false);
int offset = 0;
int length = 0;
if (has(jpeg, byteSize, 1)) {
// JPEG image files begin with FF D8. Only JPEG images have EXIF data.
final boolean possibleJpegFormat = jpeg.get(0) == (byte) 0xFF
&& jpeg.get(1) == (byte) 0xD8;
if (!possibleJpegFormat) {
return 0;
}
}
// ISO/IEC 10918-1:1993(E)
while (has(jpeg, byteSize, offset + 3) && (jpeg.get(offset++) & 0xFF) == 0xFF) {
final int marker = jpeg.get(offset) & 0xFF;
// Check if the marker is a padding.
if (marker == 0xFF) {
continue;
}
offset++;
// Check if the marker is SOI or TEM.
if (marker == 0xD8 || marker == 0x01) {
continue;
}
// Check if the marker is EOI or SOS.
if (marker == 0xD9 || marker == 0xDA) {
// Loop ends.
jpeg.advanceTo(offset - readBackwards);
break;
}
// Get the length and check if it is reasonable.
length = pack(jpeg, offset, 2, false);
if (length < 2 || !has(jpeg, byteSize, offset + length - 1)) {
Log.e(TAG, "Invalid length");
return 0;
}
// Break if the marker is EXIF in APP1.
if (marker == 0xE1 && length >= 8 &&
pack(jpeg, offset + 2, 4, false) == 0x45786966 &&
pack(jpeg, offset + 6, 2, false) == 0) {
offset += 8;
length -= 8;
// Loop ends.
jpeg.advanceTo(offset - readBackwards);
break;
}
// Skip other markers.
offset += length;
length = 0;
// Loop ends.
jpeg.advanceTo(offset - readBackwards);
}
// JEITA CP-3451 Exif Version 2.2
if (length > 8) {
// Identify the byte order.
int tag = pack(jpeg, offset, 4, false);
if (tag != 0x49492A00 && tag != 0x4D4D002A) {
Log.e(TAG, "Invalid byte order");
return 0;
}
final boolean littleEndian = (tag == 0x49492A00);
// Get the offset and check if it is reasonable.
int count = pack(jpeg, offset + 4, 4, littleEndian) + 2;
if (count < 10 || count > length) {
Log.e(TAG, "Invalid offset");
return 0;
}
offset += count;
length -= count;
// Offset has changed significantly.
jpeg.advanceTo(offset - readBackwards);
// Get the count and go through all the elements.
count = pack(jpeg, offset - 2, 2, littleEndian);
while (count-- > 0 && length >= 12) {
// Get the tag and check if it is orientation.
tag = pack(jpeg, offset, 2, littleEndian);
if (tag == 0x0112) {
// We do not really care about type and count, do we?
final int orientation = pack(jpeg, offset + 8, 2, littleEndian);
switch (orientation) {
case 1:
return 0;
case 3:
return 180;
case 6:
return 90;
case 8:
return 270;
}
Log.i(TAG, "Unsupported orientation");
return 0;
}
offset += 12;
length -= 12;
// Loop ends.
jpeg.advanceTo(offset - readBackwards);
}
}
return 0;
}
private static int pack(final InputStreamBuffer bytes, int offset, int length,
final boolean littleEndian) {
int step = 1;
if (littleEndian) {
offset += length - 1;
step = -1;
}
int value = 0;
while (length-- > 0) {
value = (value << 8) | (bytes.get(offset) & 0xFF);
offset += step;
}
return value;
}
private static boolean has(final InputStreamBuffer jpeg, final long byteSize, final int index) {
if (byteSize >= 0) {
return index < byteSize;
} else {
// For large values of index, this will cause the internal buffer to resize.
return jpeg.has(index);
}
}
@Deprecated
public static int getOrientation(final byte[] jpeg) {
return getOrientation(new ByteArrayInputStream(jpeg), jpeg.length);
}
}
@@ -0,0 +1,280 @@
/*
* Copyright (C) 2011 Google Inc.
* Licensed to 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.android.ex.photo.util;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Build;
import android.util.Base64;
import android.util.Log;
import com.android.ex.photo.PhotoViewController;
import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.regex.Pattern;
/**
* Image utilities
*/
public class ImageUtils {
// Logging
private static final String TAG = "ImageUtils";
/** Minimum class memory class to use full-res photos */
private final static long MIN_NORMAL_CLASS = 32;
/** Minimum class memory class to use small photos */
private final static long MIN_SMALL_CLASS = 24;
private static final String BASE64_URI_PREFIX = "base64,";
private static final Pattern BASE64_IMAGE_URI_PATTERN = Pattern.compile("^(?:.*;)?base64,.*");
public static enum ImageSize {
EXTRA_SMALL,
SMALL,
NORMAL,
}
public static final ImageSize sUseImageSize;
static {
// On HC and beyond, assume devices are more capable
if (Build.VERSION.SDK_INT >= 11) {
sUseImageSize = ImageSize.NORMAL;
} else {
if (PhotoViewController.sMemoryClass >= MIN_NORMAL_CLASS) {
// We have plenty of memory; use full sized photos
sUseImageSize = ImageSize.NORMAL;
} else if (PhotoViewController.sMemoryClass >= MIN_SMALL_CLASS) {
// We have slight less memory; use smaller sized photos
sUseImageSize = ImageSize.SMALL;
} else {
// We have little memory; use very small sized photos
sUseImageSize = ImageSize.EXTRA_SMALL;
}
}
}
/**
* @return true if the MimeType type is image
*/
public static boolean isImageMimeType(String mimeType) {
return mimeType != null && mimeType.startsWith("image/");
}
/**
* Create a bitmap from a local URI
*
* @param resolver The ContentResolver
* @param uri The local URI
* @param maxSize The maximum size (either width or height)
* @return The new bitmap or null
*/
public static BitmapResult createLocalBitmap(final ContentResolver resolver, final Uri uri,
final int maxSize) {
final BitmapResult result = new BitmapResult();
final InputStreamFactory factory = createInputStreamFactory(resolver, uri);
try {
final Point bounds = getImageBounds(factory);
if (bounds == null) {
result.status = BitmapResult.STATUS_EXCEPTION;
return result;
}
final BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inSampleSize = Math.max(bounds.x / maxSize, bounds.y / maxSize);
result.bitmap = decodeStream(factory, null, opts);
result.status = BitmapResult.STATUS_SUCCESS;
return result;
} catch (FileNotFoundException exception) {
// Do nothing - the photo will appear to be missing
} catch (IOException exception) {
result.status = BitmapResult.STATUS_EXCEPTION;
} catch (IllegalArgumentException exception) {
// Do nothing - the photo will appear to be missing
} catch (SecurityException exception) {
result.status = BitmapResult.STATUS_EXCEPTION;
}
return result;
}
/**
* Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect,
* BitmapFactory.Options)} that returns {@code null} on {@link
* OutOfMemoryError}.
*
* @param factory Used to create input streams that holds the raw data to be decoded into a
* bitmap.
* @param outPadding If not null, return the padding rect for the bitmap if
* it exists, otherwise set padding to [-1,-1,-1,-1]. If
* no bitmap is returned (null) then padding is
* unchanged.
* @param opts null-ok; Options that control downsampling and whether the
* image should be completely decoded, or just is size returned.
* @return The decoded bitmap, or null if the image data could not be
* decoded, or, if opts is non-null, if opts requested only the
* size be returned (in opts.outWidth and opts.outHeight)
*/
public static Bitmap decodeStream(final InputStreamFactory factory, final Rect outPadding,
final BitmapFactory.Options opts) throws FileNotFoundException {
InputStream is = null;
try {
// Determine the orientation for this image
is = factory.createInputStream();
final int orientation = Exif.getOrientation(is, -1);
if (is != null) {
is.close();
}
// Decode the bitmap
is = factory.createInputStream();
final Bitmap originalBitmap = BitmapFactory.decodeStream(is, outPadding, opts);
if (is != null && originalBitmap == null && !opts.inJustDecodeBounds) {
Log.w(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options): "
+ "Image bytes cannot be decoded into a Bitmap");
throw new UnsupportedOperationException(
"Image bytes cannot be decoded into a Bitmap.");
}
// Rotate the Bitmap based on the orientation
if (originalBitmap != null && orientation != 0) {
final Matrix matrix = new Matrix();
matrix.postRotate(orientation);
return Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(),
originalBitmap.getHeight(), matrix, true);
}
return originalBitmap;
} catch (OutOfMemoryError oome) {
Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome);
return null;
} catch (IOException ioe) {
Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an IOE", ioe);
return null;
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
// Do nothing
}
}
}
}
/**
* Gets the image bounds
*
* @param factory Used to create the InputStream.
*
* @return The image bounds
*/
private static Point getImageBounds(final InputStreamFactory factory)
throws IOException {
final BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
decodeStream(factory, null, opts);
return new Point(opts.outWidth, opts.outHeight);
}
private static InputStreamFactory createInputStreamFactory(final ContentResolver resolver,
final Uri uri) {
final String scheme = uri.getScheme();
if ("data".equals(scheme)) {
return new DataInputStreamFactory(resolver, uri);
}
return new BaseInputStreamFactory(resolver, uri);
}
/**
* Utility class for when an InputStream needs to be read multiple times. For example, one pass
* may load EXIF orientation, and the second pass may do the actual Bitmap decode.
*/
public interface InputStreamFactory {
/**
* Create a new InputStream. The caller of this method must be able to read the input
* stream starting from the beginning.
* @return
*/
InputStream createInputStream() throws FileNotFoundException;
}
private static class BaseInputStreamFactory implements InputStreamFactory {
protected final ContentResolver mResolver;
protected final Uri mUri;
public BaseInputStreamFactory(final ContentResolver resolver, final Uri uri) {
mResolver = resolver;
mUri = uri;
}
@Override
public InputStream createInputStream() throws FileNotFoundException {
return mResolver.openInputStream(mUri);
}
}
private static class DataInputStreamFactory extends BaseInputStreamFactory {
private byte[] mData;
public DataInputStreamFactory(final ContentResolver resolver, final Uri uri) {
super(resolver, uri);
}
@Override
public InputStream createInputStream() throws FileNotFoundException {
if (mData == null) {
mData = parseDataUri(mUri);
if (mData == null) {
return super.createInputStream();
}
}
return new ByteArrayInputStream(mData);
}
private byte[] parseDataUri(final Uri uri) {
final String ssp = uri.getSchemeSpecificPart();
try {
if (ssp.startsWith(BASE64_URI_PREFIX)) {
final String base64 = ssp.substring(BASE64_URI_PREFIX.length());
return Base64.decode(base64, Base64.URL_SAFE);
} else if (BASE64_IMAGE_URI_PATTERN.matcher(ssp).matches()){
final String base64 = ssp.substring(
ssp.indexOf(BASE64_URI_PREFIX) + BASE64_URI_PREFIX.length());
return Base64.decode(base64, Base64.DEFAULT);
} else {
return null;
}
} catch (IllegalArgumentException ex) {
Log.e(TAG, "Mailformed data URI: " + ex);
return null;
}
}
}
}
@@ -0,0 +1,376 @@
/*
* Copyright (C) 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.
*/
package com.android.ex.photo.util;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
/**
* Wrapper for {@link InputStream} that allows you to read bytes from it like a byte[]. An
* internal buffer is kept as small as possible to avoid large unnecessary allocations.
*
* <p/>
* Care must be taken so that the internal buffer is kept small. The best practice is to
* precalculate the maximum buffer size that you will need. For example,
* say you have a loop that reads bytes from index <code>0</code> to <code>10</code>,
* skips to index <code>N</code>, reads from index <code>N</code> to <code>N+10</code>, etc. Then
* you know that the internal buffer can have a maximum size of <code>10</code>,
* and you should set the <code>bufferSize</code> parameter to <code>10</code> in the constructor.
*
* <p/>
* Use {@link #advanceTo(int)} to declare that you will not need to access lesser indexes. This
* helps to keep the internal buffer small. In the above example, after reading bytes from index
* <code>0</code> to <code>10</code>, you should call <code>advanceTo(N)</code> so that internal
* buffer becomes filled with bytes from index <code>N</code> to <code>N+10</code>.
*
* <p/>
* If you know that you are reading bytes from a <strong>strictly</strong> increasing or equal
* index, then you should set the <code>autoAdvance</code> parameter to <code>true</code> in the
* constructor. For complicated access patterns, or when you prefer to control the internal
* buffer yourself, set <code>autoAdvance</code> to <code>false</code>. When
* <code>autoAdvance</code> is enabled, every time an index is beyond the buffer length,
* the buffer will be shifted forward such that the index requested becomes the first element in
* the buffer.
*
* <p/>
* All public methods with parameter <code>index</code> are absolute indexed. The index is from
* the beginning of the wrapped input stream.
*/
public class InputStreamBuffer {
private static final boolean DEBUG = false;
private static final int DEBUG_MAX_BUFFER_SIZE = 80;
private static final String TAG = "InputStreamBuffer";
private InputStream mInputStream;
private byte[] mBuffer;
private boolean mAutoAdvance;
/** Byte count the buffer is offset by. */
private int mOffset = 0;
/** Number of bytes filled in the buffer. */
private int mFilled = 0;
/**
* Construct a new wrapper for an InputStream.
*
* <p/>
* If <code>autoAdvance</code> is true, behavior is undefined if you call {@link #get(int)}
* or {@link #has(int)} with an index N, then some arbitrary time later call {@link #get(int)}
* or {@link #has(int)} with an index M < N. The wrapper may return the right value,
* if the buffer happens to still contain index M, but more likely it will throw an
* {@link IllegalStateException}.
*
* <p/>
* If <code>autoAdvance</code> is false, you must be diligent and call {@link #advanceTo(int)}
* at the appropriate times to ensure that the internal buffer is not unnecessarily resized
* and reallocated.
*
* @param inputStream The input stream to wrap. The input stream will not be closed by the
* wrapper.
* @param bufferSize The initial size for the internal buffer. The buffer size should be
* carefully chosen to avoid resizing and reallocating the internal buffer.
* The internal buffer size used will be the least power of two greater
* than this parameter.
* @param autoAdvance Determines the behavior when you need to read an index that is beyond
* the internal buffer size. If true, the internal buffer will shift so
* that the requested index becomes the first element. If false,
* the internal buffer size will grow to the smallest power of 2 which is
* greater than the requested index.
*/
public InputStreamBuffer(final InputStream inputStream, int bufferSize,
final boolean autoAdvance) {
mInputStream = inputStream;
if (bufferSize <= 0) {
throw new IllegalArgumentException(
String.format("Buffer size %d must be positive.", bufferSize));
}
bufferSize = leastPowerOf2(bufferSize);
mBuffer = new byte[bufferSize];
mAutoAdvance = autoAdvance;
}
/**
* Attempt to get byte at the requested index from the wrapped input stream. If the internal
* buffer contains the requested index, return immediately. If the index is less than the
* head of the buffer, or the index is greater or equal to the size of the wrapped input stream,
* a runtime exception is thrown.
*
* <p/>
* If the index is not in the internal buffer, but it can be requested from the input stream,
* {@link #fill(int)} will be called first, and the byte at the index returned.
*
* <p/>
* You should always call {@link #has(int)} with the same index, unless you are sure that no
* exceptions will be thrown as described above.
*
* <p/>
* Consider calling {@link #advanceTo(int)} if you know that you will never request a lesser
* index in the future.
* @param index The requested index.
* @return The byte at that index.
*/
public byte get(final int index) throws IllegalStateException, IndexOutOfBoundsException {
Trace.beginSection("get");
if (has(index)) {
final int i = index - mOffset;
Trace.endSection();
return mBuffer[i];
} else {
Trace.endSection();
throw new IndexOutOfBoundsException(
String.format("Index %d beyond length.", index));
}
}
/**
* Attempt to return whether the requested index is within the size of the wrapped input
* stream. One side effect is {@link #fill(int)} will be called.
*
* <p/>
* If this method returns true, it is guaranteed that {@link #get(int)} with the same index
* will not fail. That means that if the requested index is within the size of the wrapped
* input stream, but the index is less than the head of the internal buffer,
* a runtime exception is thrown.
*
* <p/>
* See {@link #get(int)} for caveats. A lot of the same warnings about exceptions and
* <code>advanceTo()</code> apply.
* @param index The requested index.
* @return True if requested index is within the size of the wrapped input stream. False if
* the index is beyond the size.
*/
public boolean has(final int index) throws IllegalStateException, IndexOutOfBoundsException {
Trace.beginSection("has");
if (index < mOffset) {
Trace.endSection();
throw new IllegalStateException(
String.format("Index %d is before buffer %d", index, mOffset));
}
final int i = index - mOffset;
// Requested index not in internal buffer.
if (i >= mFilled || i >= mBuffer.length) {
Trace.endSection();
return fill(index);
}
Trace.endSection();
return true;
}
/**
* Attempts to advance the head of the buffer to the requested index. If the index is less
* than the head of the buffer, the internal state will not be changed.
*
* <p/>
* Advancing does not fill the internal buffer. The next {@link #get(int)} or
* {@link #has(int)} call will fill the buffer.
*/
public void advanceTo(final int index) throws IllegalStateException, IndexOutOfBoundsException {
Trace.beginSection("advance to");
final int i = index - mOffset;
if (i <= 0) {
// noop
Trace.endSection();
return;
} else if (i < mFilled) {
// Shift elements starting at i to position 0.
shiftToBeginning(i);
mOffset = index;
mFilled = mFilled - i;
} else if (mInputStream != null) {
// Burn some bytes from the input stream to match the new index.
int burn = i - mFilled;
boolean empty = false;
int fails = 0;
try {
while (burn > 0) {
final long burned = mInputStream.skip(burn);
if (burned <= 0) {
fails++;
} else {
burn -= burned;
}
if (fails >= 5) {
empty = true;
break;
}
}
} catch (IOException ignored) {
empty = true;
}
if (empty) {
//Mark input stream as consumed.
mInputStream = null;
}
mOffset = index - burn;
mFilled = 0;
} else {
// Advancing beyond the input stream.
mOffset = index;
mFilled = 0;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, String.format("advanceTo %d buffer: %s", i, this));
}
Trace.endSection();
}
/**
* Attempt to fill the internal buffer fully. The buffer will be modified such that the
* requested index will always be in the buffer. If the index is less
* than the head of the buffer, a runtime exception is thrown.
*
* <p/>
* If the requested index is already in bounds of the buffer, then the buffer will just be
* filled.
*
* <p/>
* Otherwise, if <code>autoAdvance</code> was set to true in the constructor,
* {@link #advanceTo(int)} will be called with the requested index,
* and then the buffer filled. If <code>autoAdvance</code> was set to false,
* we allocate a single larger buffer of a least multiple-of-two size that can contain the
* requested index. The elements in the old buffer are copied over to the head of the new
* buffer. Then the entire buffer is filled.
* @param index The requested index.
* @return True if the byte at the requested index has been filled. False if the wrapped
* input stream ends before we reach the index.
*/
private boolean fill(final int index) {
Trace.beginSection("fill");
if (index < mOffset) {
Trace.endSection();
throw new IllegalStateException(
String.format("Index %d is before buffer %d", index, mOffset));
}
int i = index - mOffset;
// Can't fill buffer anymore if input stream is consumed.
if (mInputStream == null) {
Trace.endSection();
return false;
}
// Increase buffer size if necessary.
int length = i + 1;
if (length > mBuffer.length) {
if (mAutoAdvance) {
advanceTo(index);
i = index - mOffset;
} else {
length = leastPowerOf2(length);
Log.w(TAG, String.format(
"Increasing buffer length from %d to %d. Bad buffer size chosen, "
+ "or advanceTo() not called.",
mBuffer.length, length));
mBuffer = Arrays.copyOf(mBuffer, length);
}
}
// Read from input stream to fill buffer.
int read = -1;
try {
read = mInputStream.read(mBuffer, mFilled, mBuffer.length - mFilled);
} catch (IOException ignored) {
}
if (read != -1) {
mFilled = mFilled + read;
} else {
// Mark input stream as consumed.
mInputStream = null;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, String.format("fill %d buffer: %s", i, this));
}
Trace.endSection();
return i < mFilled;
}
/**
* Modify the internal buffer so that all the bytes are shifted towards the head by
* <code>i</code>. In other words, the byte at index <code>i</code> will now be at index
* <code>0</code>. Bytes from a lesser index are tossed.
* @param i How much to shift left.
*/
private void shiftToBeginning(final int i) {
if (i >= mBuffer.length) {
throw new IndexOutOfBoundsException(
String.format("Index %d out of bounds. Length %d", i, mBuffer.length));
}
for (int j = 0; j + i < mFilled; j++) {
mBuffer[j] = mBuffer[j + i];
}
}
@Override
public String toString() {
if (DEBUG) {
return toDebugString();
}
return String.format("+%d+%d [%d]", mOffset, mBuffer.length, mFilled);
}
public String toDebugString() {
Trace.beginSection("to debug string");
final StringBuilder sb = new StringBuilder();
sb.append("+").append(mOffset);
sb.append("+").append(mBuffer.length);
sb.append(" [");
for (int i = 0; i < mBuffer.length && i < DEBUG_MAX_BUFFER_SIZE; i++) {
if (i > 0) {
sb.append(",");
}
if (i < mFilled) {
sb.append(String.format("%02X", mBuffer[i]));
} else {
sb.append("__");
}
}
if (mInputStream != null) {
sb.append("...");
}
sb.append("]");
Trace.endSection();
return sb.toString();
}
/**
* Calculate the least power of two greater than or equal to the input.
*/
private static int leastPowerOf2(int n) {
n--;
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
n |= n >> 8;
n |= n >> 16;
n++;
return n;
}
}
@@ -0,0 +1,51 @@
/*
* Copyright (C) 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.
*/
package com.android.ex.photo.util;
import android.os.Build;
/**
* Stand-in for {@link android.os.Trace}.
*/
public abstract class Trace {
/**
* Begins systrace tracing for a given tag. No-op on unsupported platform versions.
*
* @param tag systrace tag to use
*
* @see android.os.Trace#beginSection(String)
*/
public static void beginSection(String tag) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
android.os.Trace.beginSection(tag);
}
}
/**
* Ends systrace tracing for the most recently begun section. No-op on unsupported platform
* versions.
*
* @see android.os.Trace#endSection()
*/
public static void endSection() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
android.os.Trace.endSection();
}
}
}
@@ -0,0 +1,68 @@
package com.android.ex.photo.util;
import android.content.Context;
import android.os.Build;
import androidx.core.view.accessibility.AccessibilityEventCompat;
import androidx.core.view.accessibility.AccessibilityRecordCompat;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
public class Util {
public static boolean isTouchExplorationEnabled(AccessibilityManager accessibilityManager) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
return accessibilityManager.isTouchExplorationEnabled();
} else {
return false;
}
}
/**
* Make an announcement which is related to some sort of a context change. Also see
* {@link android.view.View#announceForAccessibility}
* @param view The view that triggered the announcement
* @param accessibilityManager AccessibilityManager instance. If it is null, the method can
* obtain an instance itself.
* @param text The announcement text
*/
public static void announceForAccessibility(
final View view, AccessibilityManager accessibilityManager,
final CharSequence text) {
// Jelly Bean added support for speaking text verbatim
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
view.announceForAccessibility(text);
return;
}
final Context context = view.getContext().getApplicationContext();
if (accessibilityManager == null) {
accessibilityManager = (AccessibilityManager) context.getSystemService(
Context.ACCESSIBILITY_SERVICE);
}
if (!accessibilityManager.isEnabled()) {
return;
}
final int eventType = AccessibilityEvent.TYPE_VIEW_FOCUSED;
// Construct an accessibility event with the minimum recommended
// attributes. An event without a class name or package may be dropped.
final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
event.getText().add(text);
event.setEnabled(view.isEnabled());
event.setClassName(view.getClass().getName());
event.setPackageName(context.getPackageName());
// JellyBean MR1 requires a source view to set the window ID.
final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event);
record.setSource(view);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
view.getParent().requestSendAccessibilityEvent(view, event);
} else {
// Sends the event directly through the accessibility manager.
accessibilityManager.sendAccessibilityEvent(event);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,68 @@
/*
* Copyright (C) 2012 Google Inc.
* Licensed to 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.android.ex.photo.views;
import android.view.View;
import android.widget.ProgressBar;
/**
* This class wraps around two progress bars and is solely designed to fix
* a bug in the framework (b/6928449) that prevents a progress bar from
* gracefully switching back and forth between indeterminate and determinate
* modes.
*/
public class ProgressBarWrapper {
private final ProgressBar mDeterminate;
private final ProgressBar mIndeterminate;
private boolean mIsIndeterminate;
public ProgressBarWrapper(ProgressBar determinate,
ProgressBar indeterminate, boolean isIndeterminate) {
mDeterminate = determinate;
mIndeterminate = indeterminate;
setIndeterminate(isIndeterminate);
}
public void setIndeterminate(boolean isIndeterminate) {
mIsIndeterminate = isIndeterminate;
setVisibility(mIsIndeterminate);
}
public void setVisibility(int visibility) {
if (visibility == View.INVISIBLE || visibility == View.GONE) {
mIndeterminate.setVisibility(visibility);
mDeterminate.setVisibility(visibility);
} else {
setVisibility(mIsIndeterminate);
}
}
private void setVisibility(boolean isIndeterminate) {
mIndeterminate.setVisibility(isIndeterminate ? View.VISIBLE : View.GONE);
mDeterminate.setVisibility(isIndeterminate ? View.GONE : View.VISIBLE);
}
public void setMax(int max) {
mDeterminate.setMax(max);
}
public void setProgress(int progress) {
mDeterminate.setProgress(progress);
}
}