I'm trying to search youtube video based on given keyword. So that I can list all found video to display in a list with video thumbnail. I don't find a way to do so. After searching I found this https://developers.google.com/youtube/v3/docs/videos/list#examples.
Can I use save for my android application? Or how do I search in Android application?
You can use the Youtube API code samples in your app on the following conditions:
1. Your app is a free app.
2. If your app is monetized (paid, ads, and/or in app purchases) then your app has to contain more content and features than just a feature searching Youtube.
Full details here:
https://developers.google.com/youtube/creating_monetizable_applications
That being said, you can fully use code samples from Youtube API. I have done so myself to create an Android app to help people find all my favorite cat videos (shameless promotion): Cat Roulette
Anyhow, here is a specific Java code example for searching Youtube videos:
https://developers.google.com/youtube/v3/code_samples/java#search_by_keyword
/*
* Copyright (c) 2012 Google Inc.
*
* 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.google.api.services.samples.youtube.cmdline.data;
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.samples.youtube.cmdline.Auth;
import com.google.api.services.youtube.YouTube;
import com.google.api.services.youtube.model.ResourceId;
import com.google.api.services.youtube.model.SearchListResponse;
import com.google.api.services.youtube.model.SearchResult;
import com.google.api.services.youtube.model.Thumbnail;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
/**
* Print a list of videos matching a search term.
*
* #author Jeremy Walker
*/
public class Search {
/**
* Define a global variable that identifies the name of a file that
* contains the developer's API key.
*/
private static final String PROPERTIES_FILENAME = "youtube.properties";
private static final long NUMBER_OF_VIDEOS_RETURNED = 25;
/**
* Define a global instance of a Youtube object, which will be used
* to make YouTube Data API requests.
*/
private static YouTube youtube;
/**
* Initialize a YouTube object to search for videos on YouTube. Then
* display the name and thumbnail image of each video in the result set.
*
* #param args command line args.
*/
public static void main(String[] args) {
// Read the developer key from the properties file.
Properties properties = new Properties();
try {
InputStream in = Search.class.getResourceAsStream("/" + PROPERTIES_FILENAME);
properties.load(in);
} catch (IOException e) {
System.err.println("There was an error reading " + PROPERTIES_FILENAME + ": " + e.getCause()
+ " : " + e.getMessage());
System.exit(1);
}
try {
// This object is used to make YouTube Data API requests. The last
// argument is required, but since we don't need anything
// initialized when the HttpRequest is initialized, we override
// the interface and provide a no-op function.
youtube = new YouTube.Builder(Auth.HTTP_TRANSPORT, Auth.JSON_FACTORY, new HttpRequestInitializer() {
public void initialize(HttpRequest request) throws IOException {
}
}).setApplicationName("youtube-cmdline-search-sample").build();
// Prompt the user to enter a query term.
String queryTerm = getInputQuery();
// Define the API request for retrieving search results.
YouTube.Search.List search = youtube.search().list("id,snippet");
// Set your developer key from the Google Developers Console for
// non-authenticated requests. See:
// https://console.developers.google.com/
String apiKey = properties.getProperty("youtube.apikey");
search.setKey(apiKey);
search.setQ(queryTerm);
// Restrict the search results to only include videos. See:
// https://developers.google.com/youtube/v3/docs/search/list#type
search.setType("video");
// To increase efficiency, only retrieve the fields that the
// application uses.
search.setFields("items(id/kind,id/videoId,snippet/title,snippet/thumbnails/default/url)");
search.setMaxResults(NUMBER_OF_VIDEOS_RETURNED);
// Call the API and print results.
SearchListResponse searchResponse = search.execute();
List<SearchResult> searchResultList = searchResponse.getItems();
if (searchResultList != null) {
prettyPrint(searchResultList.iterator(), queryTerm);
}
} catch (GoogleJsonResponseException e) {
System.err.println("There was a service error: " + e.getDetails().getCode() + " : "
+ e.getDetails().getMessage());
} catch (IOException e) {
System.err.println("There was an IO error: " + e.getCause() + " : " + e.getMessage());
} catch (Throwable t) {
t.printStackTrace();
}
}
/*
* Prompt the user to enter a query term and return the user-specified term.
*/
private static String getInputQuery() throws IOException {
String inputQuery = "";
System.out.print("Please enter a search term: ");
BufferedReader bReader = new BufferedReader(new InputStreamReader(System.in));
inputQuery = bReader.readLine();
if (inputQuery.length() < 1) {
// Use the string "YouTube Developers Live" as a default.
inputQuery = "YouTube Developers Live";
}
return inputQuery;
}
/*
* Prints out all results in the Iterator. For each result, print the
* title, video ID, and thumbnail.
*
* #param iteratorSearchResults Iterator of SearchResults to print
*
* #param query Search query (String)
*/
private static void prettyPrint(Iterator<SearchResult> iteratorSearchResults, String query) {
System.out.println("\n=============================================================");
System.out.println(
" First " + NUMBER_OF_VIDEOS_RETURNED + " videos for search on \"" + query + "\".");
System.out.println("=============================================================\n");
if (!iteratorSearchResults.hasNext()) {
System.out.println(" There aren't any results for your query.");
}
while (iteratorSearchResults.hasNext()) {
SearchResult singleVideo = iteratorSearchResults.next();
ResourceId rId = singleVideo.getId();
// Confirm that the result represents a video. Otherwise, the
// item will not contain a video ID.
if (rId.getKind().equals("youtube#video")) {
Thumbnail thumbnail = singleVideo.getSnippet().getThumbnails().getDefault();
System.out.println(" Video Id" + rId.getVideoId());
System.out.println(" Title: " + singleVideo.getSnippet().getTitle());
System.out.println(" Thumbnail: " + thumbnail.getUrl());
System.out.println("\n-------------------------------------------------------------\n");
}
}
}
}
FYI you have to also obtain a Youtube API key as explained here:
https://developers.google.com/youtube/registering_an_application?hl=en
Related
I'm able to successfully add my images to google drive, using the API provided by Google Drive.
Can I now retrieve it back to my App?
If yes, Can anyone help with how to do that?
Use this to retrieve files from the Google Drive API
https://www.googleapis.com/drive/v3/files/[FILEID]?key=[YOUR_API_KEY]'
If you want the entirety of code required to retrieve a file, here it is:
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpResponse;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.model.File;
import java.io.IOException;
import java.io.InputStream;
// ...
public class MyClass {
// ...
/**
* Print a file's metadata.
*
* #param service Drive API service instance.
* #param fileId ID of the file to print metadata for.
*/
private static void printFile(Drive service, String fileId) {
try {
File file = service.files().get(fileId).execute();
System.out.println("Title: " + file.getTitle());
System.out.println("Description: " + file.getDescription());
System.out.println("MIME type: " + file.getMimeType());
} catch (IOException e) {
System.out.println("An error occurred: " + e);
}
}
/**
* Download a file's content.
*
* #param service Drive API service instance.
* #param file Drive File instance.
* #return InputStream containing the file's content if successful,
* {#code null} otherwise.
*/
private static InputStream downloadFile(Drive service, File file) {
if (file.getDownloadUrl() != null && file.getDownloadUrl().length() > 0) {
try {
HttpResponse resp =
service.getRequestFactory().buildGetRequest(new GenericUrl(file.getDownloadUrl()))
.execute();
return resp.getContent();
} catch (IOException e) {
// An error occurred.
e.printStackTrace();
return null;
}
} else {
// The file doesn't have any content stored on Drive.
return null;
}
}
// ...
}
I am new to android, I am trying to implement YouTube's search by keyword in my android app.But I am not able to get response from API (I think).I think there is some problem in search.execute().I looked some other answers too but not able to find any solution.
Here's the class (I modified it a little bit):
class Search {
private static final long NUMBER_OF_VIDEOS_RETURNED = 2;
/**
* Define a global instance of a Youtube object, which will be used
* to make YouTube Data API requests.
*/
private YouTube youtube;
/**
* Initialize a YouTube object to search for videos on YouTube. Then
* display the name and thumbnail image of each video in the result set.
*
*
*/
private void Hakuna() {
// Read the developer key from the properties file.
try {
// This object is used to make YouTube Data API requests. The last
// argument is required, but since we don't need anything
// initialized when the HttpRequest is initialized, we override
// the interface and provide a no-op function.
youtube = new YouTube.Builder(new NetHttpTransport(), new JacksonFactory(), new HttpRequestInitializer() {
public void initialize(HttpRequest request) throws IOException {
}
}).setApplicationName("youtube-cmdline-search-sample").build();
// Prompt the user to enter a query term.
String queryTerm = getInputQuery();
// Define the API request for retrieving search results.
YouTube.Search.List search = youtube.search().list("id,snippet");
// Set your developer key from the {{ Google Cloud Console }} for
// non-authenticated requests. See:
// {{ https://cloud.google.com/console }}
String apiKey = "HERE_I_PUT_MY_APIKEY";
search.setKey(apiKey);
search.setQ(queryTerm);
// Restrict the search results to only include videos. See:
// https://developers.google.com/youtube/v3/docs/search/list#type
search.setType("video");
// To increase efficiency, only retrieve the fields that the
// application uses.
search.setFields("items(id/kind,id/videoId,snippet/title,snippet/thumbnails/default/url)");
search.setMaxResults(NUMBER_OF_VIDEOS_RETURNED);
// Call the API and print results.
SearchListResponse searchResponse = search.execute();
mResult.setText("hello");
List<SearchResult> searchResultList = searchResponse.getItems();
if (searchResultList != null) {
prettyPrint(searchResultList.iterator(), queryTerm);
}
} catch (GoogleJsonResponseException e) {
System.err.println("There was a service error: " + e.getDetails().getCode() + " : "
+ e.getDetails().getMessage());
} catch (IOException e) {
System.err.println("There was an IO error: " + e.getCause() + " : " + e.getMessage());
} catch (Throwable t) {
t.printStackTrace();
}
}
/*
* Prompt the user to enter a query term and return the user-specified term.
*/
private String getInputQuery() throws IOException {
String inputQuery = "";
inputQuery = mSearchtext.getText().toString();
if (inputQuery.length() < 1) {
// Use the string "YouTube Developers Live" as a default.
inputQuery = "YouTube Developers Live";
}
return inputQuery;
}
/*
* Prints out all results in the Iterator. For each result, print the
* title, video ID, and thumbnail.
*
* #param iteratorSearchResults Iterator of SearchResults to print
*
* #param query Search query (String)
*/
private void prettyPrint(Iterator<SearchResult> iteratorSearchResults, String query) {
mResult.setText("hello");
System.out.println("\n=============================================================");
System.out.println(
" First " + NUMBER_OF_VIDEOS_RETURNED + " videos for search on \"" + query + "\".");
System.out.println("=============================================================\n");
if (!iteratorSearchResults.hasNext()) {
System.out.println(" There aren't any results for your query.");
}
while (iteratorSearchResults.hasNext()) {
SearchResult singleVideo = iteratorSearchResults.next();
ResourceId rId = singleVideo.getId();
// Confirm that the result represents a video. Otherwise, the
// item will not contain a video ID.
if (rId.getKind().equals("youtube#video")) {
Thumbnail thumbnail = singleVideo.getSnippet().getThumbnails().getDefault();
System.out.println(" Video Id" + rId.getVideoId());
System.out.println(" Title: " + singleVideo.getSnippet().getTitle());
System.out.println(" Thumbnail: " + thumbnail.getUrl());
System.out.println("\n-------------------------------------------------------------\n");
}
}
}
}
Here are the dependencies that I added:
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
implementation 'com.google.apis:google-api-services-youtube:v3-rev181-1.22.0'
There may be some redundant lines of code, please ignore them.
I have implemented inapp purchasing in my application.
When I buy a managed application it shows me this error log.
Can anyone tell me why this happen?
Here is my logcat:
08-31 14:58:00.931: E/IABUtil/Security(31515): Purchase verification failed: missing data.
08-31 14:58:00.931: E/viable(31515): Public key signature doesn't match!
and here is my Security.java Class
/* Copyright (c) 2012 Google Inc.
*
* 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 Classes;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
/**
* Security-related methods. For a secure implementation, all of this code
* should be implemented on a server that communicates with the
* application on the device. For the sake of simplicity and clarity of this
* example, this code is included here and is executed on the device. If you
* must verify the purchases on the phone, you should obfuscate this code to
* make it harder for an attacker to replace the code with stubs that treat all
* purchases as verified.
*/
class Security {
private static final String TAG = "IABUtil/Security";
private static final String KEY_FACTORY_ALGORITHM = "RSA";
private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
/**
* Verifies that the data was signed with the given signature, and returns
* the verified purchase. The data is in JSON format and signed
* with a private key. The data also contains the {#link PurchaseState}
* and product ID of the purchase.
* #param base64PublicKey the base64-encoded public key to use for verifying.
* #param signedData the signed JSON string (signed, not encrypted)
* #param signature the signature for the data, signed with the private key
*/
public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
TextUtils.isEmpty(signature)) {
Log.e(TAG, "Purchase verification failed: missing data.");
return false;
}
PublicKey key = Security.generatePublicKey(base64PublicKey);
return Security.verify(key, signedData, signature);
}
/**
* Generates a PublicKey instance from a string containing the
* Base64-encoded public key.
*
* #param encodedPublicKey Base64-encoded public key
* #throws IllegalArgumentException if encodedPublicKey is invalid
*/
public static PublicKey generatePublicKey(String encodedPublicKey) {
try {
byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (InvalidKeySpecException e) {
Log.e(TAG, "Invalid key specification.");
throw new IllegalArgumentException(e);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Base64 decoding failed.");
throw e;
}
}
/**
* Verifies that the signature from the server matches the computed
* signature on the data. Returns true if the data is correctly signed.
*
* #param publicKey public key associated with the developer account
* #param signedData signed data from server
* #param signature server signature
* #return true if the data and signature match
*/
public static boolean verify(PublicKey publicKey, String signedData, String signature) {
Signature sig;
try {
sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicKey);
sig.update(signedData.getBytes());
if (!sig.verify(Base64.decode(signature, Base64.DEFAULT))) {
Log.e(TAG, "Signature verification failed.");
return false;
}
return true;
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "NoSuchAlgorithmException.");
} catch (InvalidKeyException e) {
Log.e(TAG, "Invalid key specification.");
} catch (SignatureException e) {
Log.e(TAG, "Signature exception.");
} catch (IllegalArgumentException e) {
Log.e(TAG, "Base64 decoding failed.");
}
return false;
}
}
I think there is a problem regarding your App public key / License Key for the application.
http://developer.android.com/google/play/billing/api.html
I am attempting to build an Android app (without having to root the phone) which collects the IP addresses from all the apps' network connections to and from the phone.
I have been tasked with this project and the key point is that the phone must stay unrooted - this (to my knowledge) means I can't use tcpdump or libpcap, since both seem to need the phone to be rooted. One other point, I have found solutions where a VPN Service is used, but, if possible, I am not suppose to use this feature as well, since the app is suppose to work on it's own - without any 'outside' help.
I have been all over stack overflow and many many other sites, trying to find a method to monitor/collect IP addresses and all the solutions I've found required rooting the Android phone. This led me to believe that it wasn't possible, until I found the following app on Google Play.
https://play.google.com/store/apps/details?id=com.borgshell.connectiontrackerfree&hl=en
This app does much more than I need, but it somehow shows the IP addresses of network connections each app is making.
To summarize:
Does anyone know a way to collect IP addresses from internal app's network connections
without rooting the phone
without using a VPN service
Thank you
For all those who are curious about this same issue, I was able to finally figure out how to accomplish this task, without rooting the phone or using a VPN service.
The key to solving this issue, is to look in the following directory on the phone:
/proc/net/(tcp, tcp6, udp, udp6, etc)
Also, here is some code from an open source project that does basically what I was looking for.
/*
* Copyright (C) 2010 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 android.net.cts;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.regex.Pattern;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
public class ListeningPortsTest extends TestCase {
/** Address patterns used to check whether we're checking the right column in /proc/net. */
private static final List<String> ADDRESS_PATTERNS = new ArrayList<String>(2);
static {
ADDRESS_PATTERNS.add("[0-9A-F]{8}:[0-9A-F]{4}");
ADDRESS_PATTERNS.add("[0-9A-F]{32}:[0-9A-F]{4}");
}
/** Ports that are allowed to be listening on the emulator. */
private static final List<String> EXCEPTION_PATTERNS = new ArrayList<String>(6);
static {
// IPv4 exceptions
EXCEPTION_PATTERNS.add("00000000:15B3"); // 0.0.0.0:5555 - emulator port
EXCEPTION_PATTERNS.add("0F02000A:15B3"); // 10.0.2.15:5555 - net forwarding for emulator
EXCEPTION_PATTERNS.add("[0-9A-F]{6}7F:[0-9A-F]{4}"); // IPv4 Loopback
// IPv6 exceptions
EXCEPTION_PATTERNS.add("[0]{31}1:[0-9A-F]{4}"); // IPv6 Loopback
EXCEPTION_PATTERNS.add("[0]{16}[0]{4}[0]{4}[0-9A-F]{6}7F:[0-9A-F]{4}"); // IPv4-6 Conversion
EXCEPTION_PATTERNS.add("[0]{16}[F]{4}[0]{4}[0-9A-F]{6}7F:[0-9A-F]{4}"); // IPv4-6 Conversion
}
public void testNoListeningTcpPorts() {
assertNoListeningPorts("/proc/net/tcp", true);
}
public void testNoListeningTcp6Ports() {
assertNoListeningPorts("/proc/net/tcp6", true);
}
public void testNoListeningUdpPorts() throws Exception {
assertNoListeningUdpPorts("/proc/net/udp");
}
public void testNoListeningUdp6Ports() throws Exception {
assertNoListeningUdpPorts("/proc/net/udp6");
}
private static final int RETRIES_MAX = 6;
/**
* UDP tests can be flaky due to DNS lookups. Compensate.
*/
private static void assertNoListeningUdpPorts(String procFilePath) throws Exception {
for (int i = 0; i < RETRIES_MAX; i++) {
try {
assertNoListeningPorts(procFilePath, false);
return;
} catch (ListeningPortsAssertionError e) {
if (i == RETRIES_MAX - 1) {
throw e;
}
Thread.sleep(2 * 1000 * i);
}
}
throw new IllegalStateException("unreachable");
}
private static void assertNoListeningPorts(String procFilePath, boolean isTcp) {
/*
* Sample output of "cat /proc/net/tcp" on emulator:
*
* sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid ...
* 0: 0100007F:13AD 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 ...
* 1: 00000000:15B3 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 ...
* 2: 0F02000A:15B3 0202000A:CE8A 01 00000000:00000000 00:00000000 00000000 0 ...
*
*/
File procFile = new File(procFilePath);
Scanner scanner = null;
try {
scanner = new Scanner(procFile);
while (scanner.hasNextLine()) {
String line = scanner.nextLine().trim();
// Skip column headers
if (line.startsWith("sl")) {
continue;
}
String[] fields = line.split("\\s+");
final int expectedNumColumns = 12;
assertTrue(procFilePath + " should have at least " + expectedNumColumns
+ " columns of output " + fields, fields.length >= expectedNumColumns);
String localAddress = fields[1];
String state = fields[3];
assertTrue(procFilePath + " should have an IP address in the second column",
isAddress(localAddress));
if (!isException(localAddress) && isPortListening(state, isTcp)) {
throw new ListeningPortsAssertionError(
"Found port listening on " + localAddress + " in " + procFilePath);
}
}
} catch (FileNotFoundException notFound) {
fail("Could not open file " + procFilePath + " to check for listening ports.");
} finally {
if (scanner != null) {
scanner.close();
}
}
}
private static boolean isAddress(String localAddress) {
return isPatternMatch(ADDRESS_PATTERNS, localAddress);
}
private static boolean isException(String localAddress) {
return isPatternMatch(EXCEPTION_PATTERNS, localAddress);
}
private static boolean isPatternMatch(List<String> patterns, String input) {
for (String pattern : patterns) {
if (Pattern.matches(pattern, input)) {
return true;
}
}
return false;
}
private static boolean isPortListening(String state, boolean isTcp) {
// 0A = TCP_LISTEN from include/net/tcp_states.h
String listeningState = isTcp ? "0A" : "07";
return listeningState.equals(state);
}
private static class ListeningPortsAssertionError extends AssertionFailedError {
private ListeningPortsAssertionError(String msg) {
super(msg);
}
}
}
for my app i want to implement a changelog, but dont know how (which concept).
I want, that the changelog pops up once a time after new version of my app installed.
Sounds easy, but i have no clue. :/
Dialog to show my Changelog exists already, i just wanna know how to show it one after an update.
Thanks for your hints.
Prexx
one option is to use Android Change Log.
With Android Change Log you can easily create, show and maintain an
Android change log dialog.
Features
display only what's new or show the whole change log
display on first start of newly installed app or on new app version
write the change log in a simplified language but also use HTML and
CSS if needed
You can store a value in SharedPreferences which version you showed the changelog last time.
E.g.: 'lastChangelogVersion' : '1.1.0'
When your MainActivity starts it compares this value with the current version of your software and if it differs the changelog popup appears (and sets the new value).
This value will not be overridden when a new version of your application is being installed.
UPDATE:
Also, you might encounter that the user cleared your application's data. In this case you can't decide whether the changelog was displayed before or not so you can show it again. Android Market works the same way: if you clear it's app data you will be facing with the Licence Agreement again when launching Market.
I found the following options for adding a changelog to your Android app. Using any of these libraries would definitely save time over implementing this yourself. They all follow the general approach that #papaiatis mentions in his answer.
changeloglib
ckChangeLog
paperboy
changelog
android-change-log
Appnouncements (Disclaimer: I'm the author of this one)
I found Michael Flisar's change log (https://github.com/MFlisar/changelog) extremely easy to use.
After an app update I show a "What's New" dialog by:
ChangelogBuilder builder = new ChangelogBuilder()
.withTitle("What\'s New")
.withUseBulletList(true)
.withManagedShowOnStart(true)
.buildAndShowDialog(activity, false);
And I can show an activity with the entire change log via:
ChangelogBuilder builder = new ChangelogBuilder()
.withTitle("Change Log")
.withUseBulletList(true)
.buildAndStartActivity(context, true);
Easy peasy.
/**
* Copyright (C) 2011-2013, Karsten Priegnitz
*
* Permission to use, copy, modify, and distribute this piece of software
* for any purpose with or without fee is hereby granted, provided that
* the above copyright notice and this permission notice appear in the
* source code of all copies.
*
* It would be appreciated if you mention the author in your change log,
* contributors list or the like.
*
* #author: Karsten Priegnitz
* #see: http://code.google.com/p/android-change-log/
*/
package sheetrock.panda.changelog;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.Color;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.webkit.WebView;
public class ChangeLog {
private final Context context;
private String lastVersion, thisVersion;
// this is the key for storing the version name in SharedPreferences
private static final String VERSION_KEY = "PREFS_VERSION_KEY";
private static final String NO_VERSION = "";
/**
* Constructor
*
* Retrieves the version names and stores the new version name in SharedPreferences
*
* #param context
*/
public ChangeLog(Context context) {
this(context, PreferenceManager.getDefaultSharedPreferences(context));
}
/**
* Constructor
*
* Retrieves the version names and stores the new version name in SharedPreferences
*
* #param context
* #param sp
* the shared preferences to store the last version name into
*/
public ChangeLog(Context context, SharedPreferences sp) {
this.context = context;
// get version numbers
this.lastVersion = sp.getString(VERSION_KEY, NO_VERSION);
Log.d(TAG, "lastVersion: " + lastVersion);
try {
this.thisVersion = context.getPackageManager().getPackageInfo(context.getPackageName(),
0).versionName;
} catch (NameNotFoundException e) {
this.thisVersion = NO_VERSION;
Log.e(TAG, "could not get version name from manifest!");
e.printStackTrace();
}
Log.d(TAG, "appVersion: " + this.thisVersion);
}
/**
* #return The version name of the last installation of this app (as described in the former
* manifest). This will be the same as returned by <code>getThisVersion()</code> the
* second time this version of the app is launched (more precisely: the second time
* ChangeLog is instantiated).
* #see AndroidManifest.xml#android:versionName
*/
public String getLastVersion() {
return this.lastVersion;
}
/**
* #return The version name of this app as described in the manifest.
* #see AndroidManifest.xml#android:versionName
*/
public String getThisVersion() {
return this.thisVersion;
}
/**
* #return <code>true</code> if this version of your app is started the first time
*/
public boolean firstRun() {
return !this.lastVersion.equals(this.thisVersion);
}
/**
* #return <code>true</code> if your app including ChangeLog is started the first time ever.
* Also <code>true</code> if your app was deinstalled and installed again.
*/
public boolean firstRunEver() {
return NO_VERSION.equals(this.lastVersion);
}
/**
* #return An AlertDialog displaying the changes since the previous installed version of your
* app (what's new). But when this is the first run of your app including ChangeLog then
* the full log dialog is show.
*/
public AlertDialog getLogDialog() {
return this.getDialog(this.firstRunEver());
}
/**
* #return an AlertDialog with a full change log displayed
*/
public AlertDialog getFullLogDialog() {
return this.getDialog(true);
}
protected AlertDialog getDialog(boolean full) {
WebView wv = new WebView(this.context);
wv.setBackgroundColor(Color.parseColor(context.getResources().getString(
R.string.background_color)));
wv.loadDataWithBaseURL(null, this.getLog(full), "text/html", "UTF-8", null);
AlertDialog.Builder builder = new AlertDialog.Builder(new ContextThemeWrapper(this.context,
android.R.style.Theme_Dialog));
builder.setTitle(
context.getResources().getString(
full ? R.string.changelog_full_title : R.string.changelog_title))
.setView(wv)
.setCancelable(false)
// OK button
.setPositiveButton(context.getResources().getString(R.string.changelog_ok_button),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
updateVersionInPreferences();
}
});
if (!full) {
// "more ..." button
builder.setNegativeButton(R.string.changelog_show_full,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
getFullLogDialog().show();
}
});
}
return builder.create();
}
protected void updateVersionInPreferences() {
// save new version number to preferences
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sp.edit();
editor.putString(VERSION_KEY, thisVersion);
// // on SDK-Versions > 9 you should use this:
// if(Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
// editor.commit();
// } else {
// editor.apply();
// }
editor.commit();
}
/**
* #return HTML displaying the changes since the previous installed version of your app (what's
* new)
*/
public String getLog() {
return this.getLog(false);
}
/**
* #return HTML which displays full change log
*/
public String getFullLog() {
return this.getLog(true);
}
/** modes for HTML-Lists (bullet, numbered) */
private enum Listmode {
NONE, ORDERED, UNORDERED,
};
private Listmode listMode = Listmode.NONE;
private StringBuffer sb = null;
private static final String EOCL = "END_OF_CHANGE_LOG";
protected String getLog(boolean full) {
// read changelog.txt file
sb = new StringBuffer();
try {
InputStream ins = context.getResources().openRawResource(R.raw.changelog);
BufferedReader br = new BufferedReader(new InputStreamReader(ins));
String line = null;
boolean advanceToEOVS = false; // if true: ignore further version
// sections
while ((line = br.readLine()) != null) {
line = line.trim();
char marker = line.length() > 0 ? line.charAt(0) : 0;
if (marker == '$') {
// begin of a version section
this.closeList();
String version = line.substring(1).trim();
// stop output?
if (!full) {
if (this.lastVersion.equals(version)) {
advanceToEOVS = true;
} else if (version.equals(EOCL)) {
advanceToEOVS = false;
}
}
} else if (!advanceToEOVS) {
switch (marker) {
case '%':
// line contains version title
this.closeList();
sb.append("<div class='title'>" + line.substring(1).trim() + "</div>\n");
break;
case '_':
// line contains version title
this.closeList();
sb.append("<div class='subtitle'>" + line.substring(1).trim() + "</div>\n");
break;
case '!':
// line contains free text
this.closeList();
sb.append("<div class='freetext'>" + line.substring(1).trim() + "</div>\n");
break;
case '#':
// line contains numbered list item
this.openList(Listmode.ORDERED);
sb.append("<li>" + line.substring(1).trim() + "</li>\n");
break;
case '*':
// line contains bullet list item
this.openList(Listmode.UNORDERED);
sb.append("<li>" + line.substring(1).trim() + "</li>\n");
break;
default:
// no special character: just use line as is
this.closeList();
sb.append(line + "\n");
}
}
}
this.closeList();
br.close();
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
protected void openList(Listmode listMode) {
if (this.listMode != listMode) {
closeList();
if (listMode == Listmode.ORDERED) {
sb.append("<div class='list'><ol>\n");
} else if (listMode == Listmode.UNORDERED) {
sb.append("<div class='list'><ul>\n");
}
this.listMode = listMode;
}
}
protected void closeList() {
if (this.listMode == Listmode.ORDERED) {
sb.append("</ol></div>\n");
} else if (this.listMode == Listmode.UNORDERED) {
sb.append("</ul></div>\n");
}
this.listMode = Listmode.NONE;
}
private static final String TAG = "ChangeLog";
/**
* manually set the last version name - for testing purposes only
*
* #param lastVersion
*/
public void dontuseSetLastVersion(String lastVersion) {
this.lastVersion = lastVersion;
}
}